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:
@@ -8,7 +8,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import curses
|
import curses
|
||||||
import speechd # Python bindings for Speech Dispatcher
|
import speechd # Python bindings for Speech Dispatcher
|
||||||
import configparser
|
from stormux_speech_settings import load_speech_settings, save_speech_settings
|
||||||
|
|
||||||
class VoicedDiskMenu:
|
class VoicedDiskMenu:
|
||||||
def __init__(self, title="Apple 2e Disk Menu"):
|
def __init__(self, title="Apple 2e Disk Menu"):
|
||||||
@@ -18,11 +18,6 @@ class VoicedDiskMenu:
|
|||||||
self.stdscr = None
|
self.stdscr = None
|
||||||
self.curses_initialized = False # Flag to track if curses has been initialized
|
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
|
# Default settings
|
||||||
self.speech_rate = 0 # Normal speech rate (0 is default in speechd)
|
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
|
# Fallback to None - the speak method will handle this
|
||||||
|
|
||||||
def load_settings(self):
|
def load_settings(self):
|
||||||
"""Load settings from config file"""
|
"""Load speech settings from the shared Speech Dispatcher config."""
|
||||||
# Create default settings if they don't exist
|
self.speech_rate = load_speech_settings().rate
|
||||||
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
|
|
||||||
|
|
||||||
def save_settings(self):
|
def save_settings(self):
|
||||||
"""Save settings to config file"""
|
"""Save speech settings to the shared Speech Dispatcher config."""
|
||||||
# Ensure config directory exists
|
speechSettings = load_speech_settings()
|
||||||
os.makedirs(self.config_dir, exist_ok=True)
|
speechSettings.rate = self.speech_rate
|
||||||
|
save_speech_settings(speechSettings)
|
||||||
# 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}")
|
|
||||||
|
|
||||||
def increase_speech_rate(self):
|
def increase_speech_rate(self):
|
||||||
"""Increase speech rate"""
|
"""Increase speech rate"""
|
||||||
@@ -87,7 +57,7 @@ class VoicedDiskMenu:
|
|||||||
if self.speech_client:
|
if self.speech_client:
|
||||||
try:
|
try:
|
||||||
self.speech_client.set_rate(self.speech_rate)
|
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:
|
except Exception as e:
|
||||||
print(f"Error adjusting speech rate: {e}")
|
print(f"Error adjusting speech rate: {e}")
|
||||||
|
|
||||||
@@ -100,7 +70,7 @@ class VoicedDiskMenu:
|
|||||||
if self.speech_client:
|
if self.speech_client:
|
||||||
try:
|
try:
|
||||||
self.speech_client.set_rate(self.speech_rate)
|
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:
|
except Exception as e:
|
||||||
print(f"Error adjusting speech rate: {e}")
|
print(f"Error adjusting speech rate: {e}")
|
||||||
|
|
||||||
@@ -173,8 +143,8 @@ class VoicedDiskMenu:
|
|||||||
Down arrow: Next disk.
|
Down arrow: Next disk.
|
||||||
Enter: Launch selected disk.
|
Enter: Launch selected disk.
|
||||||
H key: Hear these instructions again.
|
H key: Hear these instructions again.
|
||||||
Left bracket: Decrease speech rate.
|
Left bracket: Decrease system speech rate.
|
||||||
Right bracket: Increase speech rate.
|
Right bracket: Increase system speech rate.
|
||||||
Escape or Q: Exit the menu.
|
Escape or Q: Exit the menu.
|
||||||
Any key will interrupt speech.
|
Any key will interrupt speech.
|
||||||
"""
|
"""
|
||||||
@@ -230,7 +200,7 @@ class VoicedDiskMenu:
|
|||||||
self.stdscr.addstr(y, x, text, attr)
|
self.stdscr.addstr(y, x, text, attr)
|
||||||
|
|
||||||
# Draw speech rate indicator
|
# 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.addstr(h-2, 2, rateText)
|
||||||
|
|
||||||
self.stdscr.refresh()
|
self.stdscr.refresh()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
import configparser
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -10,6 +9,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from stormux_speech_settings import load_speech_settings
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import simpleaudio as sa
|
import simpleaudio as sa
|
||||||
@@ -257,9 +257,6 @@ class AudioManagerApp:
|
|||||||
self.backend = AudioBackend()
|
self.backend = AudioBackend()
|
||||||
self.feedback = AudioFeedback()
|
self.feedback = AudioFeedback()
|
||||||
self.speech_client = None
|
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_rate = 0
|
||||||
self.speech_pitch = 0
|
self.speech_pitch = 0
|
||||||
self.stdscr = None
|
self.stdscr = None
|
||||||
@@ -285,16 +282,9 @@ class AudioManagerApp:
|
|||||||
self.load_speech_settings()
|
self.load_speech_settings()
|
||||||
|
|
||||||
def load_speech_settings(self):
|
def load_speech_settings(self):
|
||||||
if not os.path.exists(self.config_file):
|
speech_settings = load_speech_settings()
|
||||||
return
|
self.speech_rate = speech_settings.rate
|
||||||
|
self.speech_pitch = speech_settings.pitch
|
||||||
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
|
|
||||||
|
|
||||||
def init_speech(self):
|
def init_speech(self):
|
||||||
if speechd is None:
|
if speechd is None:
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ fi
|
|||||||
|
|
||||||
# File system trim
|
# File system trim
|
||||||
rootPartition=$(findmnt / -o SOURCE -n)
|
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
|
if ! systemctl is-enabled fstrim.timer ; then
|
||||||
sudo systemctl enable fstrim.timer
|
sudo systemctl enable fstrim.timer
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import json
|
|||||||
import shlex
|
import shlex
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
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")
|
INSTALL_GAME_RUNNER = Path("/home/stormux/.local/.functions/install_game.sh")
|
||||||
@@ -219,6 +220,10 @@ class VoicedMenu:
|
|||||||
|
|
||||||
def load_settings(self):
|
def load_settings(self):
|
||||||
"""Load settings from config file"""
|
"""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
|
# Create default settings if they don't exist
|
||||||
if not os.path.exists(self.configFile):
|
if not os.path.exists(self.configFile):
|
||||||
self.save_settings()
|
self.save_settings()
|
||||||
@@ -227,11 +232,6 @@ class VoicedMenu:
|
|||||||
try:
|
try:
|
||||||
self.config.read(self.configFile)
|
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
|
# Load volume settings
|
||||||
if 'Volume' in self.config:
|
if 'Volume' in self.config:
|
||||||
self.volume = self.config.getint('Volume', 'level', fallback=50)
|
self.volume = self.config.getint('Volume', 'level', fallback=50)
|
||||||
@@ -240,17 +240,10 @@ class VoicedMenu:
|
|||||||
# If loading fails, we'll use default values
|
# If loading fails, we'll use default values
|
||||||
|
|
||||||
def save_settings(self):
|
def save_settings(self):
|
||||||
"""Save settings to config file"""
|
"""Save menu-local settings to config file."""
|
||||||
# Ensure config directory exists
|
# Ensure config directory exists
|
||||||
os.makedirs(self.configDir, exist_ok=True)
|
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
|
# Save volume settings
|
||||||
if 'Volume' not in self.config:
|
if 'Volume' not in self.config:
|
||||||
self.config['Volume'] = {}
|
self.config['Volume'] = {}
|
||||||
@@ -264,18 +257,25 @@ class VoicedMenu:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error saving settings: {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):
|
def increase_speech_rate(self):
|
||||||
"""Increase speech rate"""
|
"""Increase speech rate"""
|
||||||
self.speechRate = min(100, self.speechRate + 10) # Max is 100
|
self.speechRate = min(100, self.speechRate + 10) # Max is 100
|
||||||
if self.speechClient:
|
if self.speechClient:
|
||||||
try:
|
try:
|
||||||
self.speechClient.set_rate(self.speechRate)
|
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:
|
except Exception as e:
|
||||||
print(f"Error adjusting speech rate: {e}")
|
print(f"Error adjusting speech rate: {e}")
|
||||||
|
|
||||||
# Save the new setting
|
# Save the new setting
|
||||||
self.save_settings()
|
self.save_speech_settings()
|
||||||
|
|
||||||
def decrease_speech_rate(self):
|
def decrease_speech_rate(self):
|
||||||
"""Decrease speech rate"""
|
"""Decrease speech rate"""
|
||||||
@@ -283,12 +283,12 @@ class VoicedMenu:
|
|||||||
if self.speechClient:
|
if self.speechClient:
|
||||||
try:
|
try:
|
||||||
self.speechClient.set_rate(self.speechRate)
|
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:
|
except Exception as e:
|
||||||
print(f"Error adjusting speech rate: {e}")
|
print(f"Error adjusting speech rate: {e}")
|
||||||
|
|
||||||
# Save the new setting
|
# Save the new setting
|
||||||
self.save_settings()
|
self.save_speech_settings()
|
||||||
|
|
||||||
def increase_speech_pitch(self):
|
def increase_speech_pitch(self):
|
||||||
"""Increase speech pitch"""
|
"""Increase speech pitch"""
|
||||||
@@ -296,12 +296,12 @@ class VoicedMenu:
|
|||||||
if self.speechClient:
|
if self.speechClient:
|
||||||
try:
|
try:
|
||||||
self.speechClient.set_pitch(self.speechPitch)
|
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:
|
except Exception as e:
|
||||||
print(f"Error adjusting speech pitch: {e}")
|
print(f"Error adjusting speech pitch: {e}")
|
||||||
|
|
||||||
# Save the new setting
|
# Save the new setting
|
||||||
self.save_settings()
|
self.save_speech_settings()
|
||||||
|
|
||||||
def decrease_speech_pitch(self):
|
def decrease_speech_pitch(self):
|
||||||
"""Decrease speech pitch"""
|
"""Decrease speech pitch"""
|
||||||
@@ -309,12 +309,12 @@ class VoicedMenu:
|
|||||||
if self.speechClient:
|
if self.speechClient:
|
||||||
try:
|
try:
|
||||||
self.speechClient.set_pitch(self.speechPitch)
|
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:
|
except Exception as e:
|
||||||
print(f"Error adjusting speech pitch: {e}")
|
print(f"Error adjusting speech pitch: {e}")
|
||||||
|
|
||||||
# Save the new setting
|
# Save the new setting
|
||||||
self.save_settings()
|
self.save_speech_settings()
|
||||||
|
|
||||||
def get_current_volume(self):
|
def get_current_volume(self):
|
||||||
"""Get the current system volume percentage"""
|
"""Get the current system volume percentage"""
|
||||||
@@ -846,8 +846,8 @@ class VoicedMenu:
|
|||||||
# Now clean up resources
|
# Now clean up resources
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
|
|
||||||
# Try to kill speech-dispatcher if needed
|
# Try to kill the user's speech-dispatcher if needed
|
||||||
subprocess.run(["sudo", "killall", "speech-dispatcher"], check=False)
|
subprocess.run(["killall", "speech-dispatcher"], check=False)
|
||||||
|
|
||||||
# Restart the application
|
# Restart the application
|
||||||
os.execv(sys.argv[0], sys.argv)
|
os.execv(sys.argv[0], sys.argv)
|
||||||
@@ -1144,10 +1144,10 @@ class VoicedMenu:
|
|||||||
Enter: Launch selected item.
|
Enter: Launch selected item.
|
||||||
Control H: Hear these instructions again.
|
Control H: Hear these instructions again.
|
||||||
Control B: Report battery status.
|
Control B: Report battery status.
|
||||||
Left bracket: Decrease speech rate.
|
Left bracket: Decrease system speech rate.
|
||||||
Right bracket: Increase speech rate.
|
Right bracket: Increase system speech rate.
|
||||||
Left brace: Decrease speech pitch.
|
Left brace: Decrease system speech pitch.
|
||||||
Right brace: Increase speech pitch.
|
Right brace: Increase system speech pitch.
|
||||||
9 key: Decrease volume.
|
9 key: Decrease volume.
|
||||||
0 key: Increase volume.
|
0 key: Increase volume.
|
||||||
Escape: Go back from submenus or refresh the main menu.
|
Escape: Go back from submenus or refresh the main menu.
|
||||||
@@ -1198,11 +1198,11 @@ class VoicedMenu:
|
|||||||
self.stdscr.addstr(y, x, text, attr)
|
self.stdscr.addstr(y, x, text, attr)
|
||||||
|
|
||||||
# Draw speech rate indicator
|
# 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.addstr(h-2, 2, rateText)
|
||||||
|
|
||||||
# Draw speech pitch indicator
|
# Draw speech pitch indicator
|
||||||
pitchText = f"Pitch: {self.speechPitch}"
|
pitchText = f"System Pitch: {self.speechPitch}"
|
||||||
pitchX = max(0, w // 2 - len(pitchText) // 2)
|
pitchX = max(0, w // 2 - len(pitchText) // 2)
|
||||||
self.stdscr.addstr(h-2, pitchX, pitchText)
|
self.stdscr.addstr(h-2, pitchX, pitchText)
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import configparser
|
|||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
import random
|
import random
|
||||||
|
from stormux_speech_settings import load_speech_settings, save_speech_settings
|
||||||
|
|
||||||
class VoicedMusicPlayer:
|
class VoicedMusicPlayer:
|
||||||
def __init__(self, title="Stormux Music Player"):
|
def __init__(self, title="Stormux Music Player"):
|
||||||
@@ -55,15 +56,14 @@ class VoicedMusicPlayer:
|
|||||||
print(f"Could not initialize speech: {e}")
|
print(f"Could not initialize speech: {e}")
|
||||||
|
|
||||||
def load_settings(self):
|
def load_settings(self):
|
||||||
|
self.speechRate = load_speech_settings().rate
|
||||||
|
|
||||||
if not os.path.exists(self.configFile):
|
if not os.path.exists(self.configFile):
|
||||||
self.save_settings()
|
self.save_settings()
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.config.read(self.configFile)
|
self.config.read(self.configFile)
|
||||||
|
|
||||||
if 'Speech' in self.config:
|
|
||||||
self.speechRate = self.config.getint('Speech', 'rate', fallback=0)
|
|
||||||
|
|
||||||
if 'Player' in self.config:
|
if 'Player' in self.config:
|
||||||
self.randomize = self.config.getboolean('Player', 'randomize', fallback=False)
|
self.randomize = self.config.getboolean('Player', 'randomize', fallback=False)
|
||||||
@@ -73,13 +73,9 @@ class VoicedMusicPlayer:
|
|||||||
def save_settings(self):
|
def save_settings(self):
|
||||||
os.makedirs(self.configDir, exist_ok=True)
|
os.makedirs(self.configDir, exist_ok=True)
|
||||||
|
|
||||||
if 'Speech' not in self.config:
|
|
||||||
self.config['Speech'] = {}
|
|
||||||
|
|
||||||
if 'Player' not in self.config:
|
if 'Player' not in self.config:
|
||||||
self.config['Player'] = {}
|
self.config['Player'] = {}
|
||||||
|
|
||||||
self.config['Speech']['rate'] = str(self.speechRate)
|
|
||||||
self.config['Player']['randomize'] = str(self.randomize)
|
self.config['Player']['randomize'] = str(self.randomize)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -88,6 +84,11 @@ class VoicedMusicPlayer:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error saving settings: {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):
|
def toggle_randomize(self):
|
||||||
self.randomize = not self.randomize
|
self.randomize = not self.randomize
|
||||||
self.save_settings()
|
self.save_settings()
|
||||||
@@ -101,22 +102,22 @@ class VoicedMusicPlayer:
|
|||||||
if self.speechClient:
|
if self.speechClient:
|
||||||
try:
|
try:
|
||||||
self.speechClient.set_rate(self.speechRate)
|
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:
|
except Exception as e:
|
||||||
print(f"Error adjusting speech rate: {e}")
|
print(f"Error adjusting speech rate: {e}")
|
||||||
|
|
||||||
self.save_settings()
|
self.save_speech_settings()
|
||||||
|
|
||||||
def decrease_speech_rate(self):
|
def decrease_speech_rate(self):
|
||||||
self.speechRate = max(-100, self.speechRate - 10)
|
self.speechRate = max(-100, self.speechRate - 10)
|
||||||
if self.speechClient:
|
if self.speechClient:
|
||||||
try:
|
try:
|
||||||
self.speechClient.set_rate(self.speechRate)
|
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:
|
except Exception as e:
|
||||||
print(f"Error adjusting speech rate: {e}")
|
print(f"Error adjusting speech rate: {e}")
|
||||||
|
|
||||||
self.save_settings()
|
self.save_speech_settings()
|
||||||
|
|
||||||
def add_section(self, sectionName):
|
def add_section(self, sectionName):
|
||||||
if sectionName not in self.menuSections:
|
if sectionName not in self.menuSections:
|
||||||
@@ -337,8 +338,8 @@ class VoicedMusicPlayer:
|
|||||||
Backspace: Go back to previous view.
|
Backspace: Go back to previous view.
|
||||||
R key: Toggle random playback.
|
R key: Toggle random playback.
|
||||||
H key: Hear these instructions again.
|
H key: Hear these instructions again.
|
||||||
Left bracket: Decrease speech rate.
|
Left bracket: Decrease system speech rate.
|
||||||
Right bracket: Increase speech rate.
|
Right bracket: Increase system speech rate.
|
||||||
Escape or Q: Exit the menu.
|
Escape or Q: Exit the menu.
|
||||||
Any key will interrupt speech.
|
Any key will interrupt speech.
|
||||||
"""
|
"""
|
||||||
@@ -352,7 +353,7 @@ class VoicedMusicPlayer:
|
|||||||
x = max(0, w // 2 - len(title) // 2)
|
x = max(0, w // 2 - len(title) // 2)
|
||||||
self.stdscr.addstr(1, x, title, curses.A_BOLD)
|
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)
|
x = max(0, w // 2 - len(helpText) // 2)
|
||||||
self.stdscr.addstr(3, x, helpText)
|
self.stdscr.addstr(3, x, helpText)
|
||||||
|
|
||||||
@@ -395,7 +396,7 @@ class VoicedMusicPlayer:
|
|||||||
x = max(0, w // 2 - len(text) // 2)
|
x = max(0, w // 2 - len(text) // 2)
|
||||||
self.stdscr.addstr(y, x, text, attr)
|
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.addstr(h-2, 2, rateText)
|
||||||
|
|
||||||
self.stdscr.refresh()
|
self.stdscr.refresh()
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import signal
|
|||||||
import curses
|
import curses
|
||||||
import subprocess
|
import subprocess
|
||||||
import speechd # Python bindings for Speech Dispatcher
|
import speechd # Python bindings for Speech Dispatcher
|
||||||
import configparser
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
from stormux_speech_settings import load_speech_settings, save_speech_settings
|
||||||
|
|
||||||
class VoicedMenu:
|
class VoicedMenu:
|
||||||
def __init__(self, title="Stormux Game Menu"):
|
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.curses_initialized = False # Flag to track if curses has been initialized
|
||||||
self.has_items = False # Flag to track if any section has items
|
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
|
# Default settings
|
||||||
self.speechRate = 0 # Normal speech rate (0 is default in speechd)
|
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
|
# Fallback to None - the speak method will handle this
|
||||||
|
|
||||||
def load_settings(self):
|
def load_settings(self):
|
||||||
"""Load settings from config file"""
|
"""Load speech settings from the shared Speech Dispatcher config."""
|
||||||
# Create default settings if they don't exist
|
self.speechRate = load_speech_settings().rate
|
||||||
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
|
|
||||||
|
|
||||||
def save_settings(self):
|
def save_settings(self):
|
||||||
"""Save settings to config file"""
|
"""Save speech settings to the shared Speech Dispatcher config."""
|
||||||
# Ensure config directory exists
|
speechSettings = load_speech_settings()
|
||||||
os.makedirs(self.configDir, exist_ok=True)
|
speechSettings.rate = self.speechRate
|
||||||
|
save_speech_settings(speechSettings)
|
||||||
# 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}")
|
|
||||||
|
|
||||||
def increase_speech_rate(self):
|
def increase_speech_rate(self):
|
||||||
"""Increase speech rate"""
|
"""Increase speech rate"""
|
||||||
@@ -96,7 +66,7 @@ class VoicedMenu:
|
|||||||
if self.speechClient:
|
if self.speechClient:
|
||||||
try:
|
try:
|
||||||
self.speechClient.set_rate(self.speechRate)
|
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:
|
except Exception as e:
|
||||||
print(f"Error adjusting speech rate: {e}")
|
print(f"Error adjusting speech rate: {e}")
|
||||||
|
|
||||||
@@ -109,7 +79,7 @@ class VoicedMenu:
|
|||||||
if self.speechClient:
|
if self.speechClient:
|
||||||
try:
|
try:
|
||||||
self.speechClient.set_rate(self.speechRate)
|
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:
|
except Exception as e:
|
||||||
print(f"Error adjusting speech rate: {e}")
|
print(f"Error adjusting speech rate: {e}")
|
||||||
|
|
||||||
@@ -226,8 +196,8 @@ class VoicedMenu:
|
|||||||
Right arrow: Next section.
|
Right arrow: Next section.
|
||||||
Enter: Launch selected item.
|
Enter: Launch selected item.
|
||||||
H key: Hear these instructions again.
|
H key: Hear these instructions again.
|
||||||
Left bracket: Decrease speech rate.
|
Left bracket: Decrease system speech rate.
|
||||||
Right bracket: Increase speech rate.
|
Right bracket: Increase system speech rate.
|
||||||
Escape or Q: Exit the menu.
|
Escape or Q: Exit the menu.
|
||||||
Any key will interrupt speech.
|
Any key will interrupt speech.
|
||||||
"""
|
"""
|
||||||
@@ -280,7 +250,7 @@ class VoicedMenu:
|
|||||||
self.stdscr.addstr(y, x, text, attr)
|
self.stdscr.addstr(y, x, text, attr)
|
||||||
|
|
||||||
# Draw speech rate indicator
|
# 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.addstr(h-2, 2, rateText)
|
||||||
|
|
||||||
self.stdscr.refresh()
|
self.stdscr.refresh()
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ export DIALOGOPTS='--insecure --no-lines --visit-items'
|
|||||||
set_timezone() {
|
set_timezone() {
|
||||||
# Get the list of timezones
|
# Get the list of timezones
|
||||||
mapfile -t regions < <(timedatectl --no-pager list-timezones | cut -d '/' -f1 | sort -u)
|
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.
|
# Use the same text twice here and just hide the tag field.
|
||||||
region=$(dialog --backtitle "Please select your Region" \
|
region=$(dialog --backtitle "Please select your Region" \
|
||||||
--no-tags \
|
--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 \
|
--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
|
if [[ -z "$region" ]]; then
|
||||||
echo "No region selected"
|
echo "No region selected"
|
||||||
@@ -21,12 +25,16 @@ set_timezone() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
mapfile -t cities < <(timedatectl --no-pager list-timezones | grep "$region" | cut -d '/' -f2 | sort -u)
|
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.
|
# Use the same text twice here and just hide the tag field.
|
||||||
city=$(dialog --backtitle "Please select a city near you" \
|
city=$(dialog --backtitle "Please select a city near you" \
|
||||||
--no-tags \
|
--no-tags \
|
||||||
--menu "Use up and down arrow or page-up and page-down to navigate the list." 0 0 10 \
|
--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
|
if [[ -z "$city" ]]; then
|
||||||
echo "No city selected"
|
echo "No city selected"
|
||||||
@@ -37,7 +45,7 @@ set_timezone() {
|
|||||||
if [[ -f /etc/localtime ]]; then
|
if [[ -f /etc/localtime ]]; then
|
||||||
rm /etc/localtime
|
rm /etc/localtime
|
||||||
fi
|
fi
|
||||||
ln -sf /usr/share/zoneinfo/${region}/${city} /etc/localtime
|
ln -sf "/usr/share/zoneinfo/${region}/${city}" /etc/localtime
|
||||||
timedatectl set-ntp true
|
timedatectl set-ntp true
|
||||||
|
|
||||||
echo "Timezone set to ${region}/${city}"
|
echo "Timezone set to ${region}/${city}"
|
||||||
@@ -49,4 +57,4 @@ if [[ $EUID -ne 0 ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
set_timezone
|
set_timezone
|
||||||
|
|||||||
@@ -10,6 +10,37 @@ import curses
|
|||||||
import speechd
|
import speechd
|
||||||
import subprocess
|
import subprocess
|
||||||
import configparser
|
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:
|
class VoiceSelectionMenu:
|
||||||
def __init__(self, title="Speech Dispatcher Voice Selection"):
|
def __init__(self, title="Speech Dispatcher Voice Selection"):
|
||||||
@@ -146,42 +177,12 @@ class VoiceSelectionMenu:
|
|||||||
try:
|
try:
|
||||||
# Clean up before executing system commands
|
# Clean up before executing system commands
|
||||||
self.cleanup(full_cleanup=False)
|
self.cleanup(full_cleanup=False)
|
||||||
|
set_default_module_in_config(SPEECHD_CONFIG, module)
|
||||||
# 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)
|
|
||||||
|
|
||||||
# Restart speech-dispatcher more thoroughly
|
# 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
|
# 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
|
# Re-initialize speech after changes
|
||||||
time.sleep(2) # Give more time for the service to restart
|
time.sleep(2) # Give more time for the service to restart
|
||||||
|
|||||||
@@ -3,51 +3,28 @@
|
|||||||
|
|
||||||
# Self-voiced Speech Rate Configuration Menu
|
# Self-voiced Speech Rate Configuration Menu
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
import curses
|
import curses
|
||||||
import speechd # Python bindings for Speech Dispatcher
|
import speechd # Python bindings for Speech Dispatcher
|
||||||
import re
|
|
||||||
import subprocess
|
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):
|
def update_speechd_config_content(content, rate, volume, pitch):
|
||||||
"""Return speechd.conf content with updated default voice parameters."""
|
"""Return speechd.conf content with updated default voice parameters."""
|
||||||
newContent = content
|
return update_shared_speechd_config_content(
|
||||||
|
content,
|
||||||
replacements = (
|
SpeechSettings(rate=rate, volume=volume, pitch=pitch),
|
||||||
("DefaultRate", rate),
|
|
||||||
("DefaultVolume", volume),
|
|
||||||
("DefaultPitch", pitch),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for settingName, settingValue in replacements:
|
|
||||||
activePattern = rf'^(\s*){settingName}\s+(-?\d+)'
|
|
||||||
commentedPattern = rf'^(\s*)#\s*{settingName}\s+(-?\d+)'
|
|
||||||
replacement = rf'\1{settingName} {settingValue}'
|
|
||||||
|
|
||||||
if re.search(activePattern, newContent, re.MULTILINE):
|
|
||||||
newContent = re.sub(
|
|
||||||
activePattern,
|
|
||||||
replacement,
|
|
||||||
newContent,
|
|
||||||
flags=re.MULTILINE,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
newContent = re.sub(
|
|
||||||
commentedPattern,
|
|
||||||
replacement,
|
|
||||||
newContent,
|
|
||||||
flags=re.MULTILINE,
|
|
||||||
)
|
|
||||||
|
|
||||||
return newContent
|
|
||||||
|
|
||||||
|
|
||||||
def get_writable_config_targets(configTargets):
|
|
||||||
"""Return existing config paths to update, preserving order."""
|
|
||||||
return [configPath for configPath in configTargets if os.path.exists(configPath)]
|
|
||||||
|
|
||||||
|
|
||||||
class SpeechRateMenu:
|
class SpeechRateMenu:
|
||||||
def __init__(self, title="Speech Configuration"):
|
def __init__(self, title="Speech Configuration"):
|
||||||
@@ -59,11 +36,8 @@ class SpeechRateMenu:
|
|||||||
self.modes = ["Rate", "Volume", "Pitch"]
|
self.modes = ["Rate", "Volume", "Pitch"]
|
||||||
self.stdscr = None
|
self.stdscr = None
|
||||||
self.cursesInitialized = False # Flag to track if curses has been initialized
|
self.cursesInitialized = False # Flag to track if curses has been initialized
|
||||||
self.configFile = "/etc/speech-dispatcher/speechd.conf"
|
self.configFile = str(SPEECHD_CONFIG)
|
||||||
self.configTargets = [
|
self.configTargets = [SPEECHD_CONFIG, FEX_SPEECHD_CONFIG]
|
||||||
self.configFile,
|
|
||||||
os.path.expanduser("~/.fex-emu/RootFS/ArchLinux/etc/speech-dispatcher/speechd.conf"),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Load current settings from config FIRST
|
# Load current settings from config FIRST
|
||||||
self.load_current_settings()
|
self.load_current_settings()
|
||||||
@@ -90,35 +64,10 @@ class SpeechRateMenu:
|
|||||||
def load_current_settings(self):
|
def load_current_settings(self):
|
||||||
"""Load the current default settings from speechd.conf"""
|
"""Load the current default settings from speechd.conf"""
|
||||||
try:
|
try:
|
||||||
with open(self.configFile, 'r') as f:
|
settings = load_speech_settings(self.configFile)
|
||||||
content = f.read()
|
self.currentRate = settings.rate
|
||||||
|
self.currentVolume = settings.volume
|
||||||
# Load Rate
|
self.currentPitch = settings.pitch
|
||||||
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:
|
except Exception:
|
||||||
# If loading fails, we'll use default values
|
# If loading fails, we'll use default values
|
||||||
@@ -127,26 +76,14 @@ class SpeechRateMenu:
|
|||||||
def save_settings_to_config(self):
|
def save_settings_to_config(self):
|
||||||
"""Save the current settings to the speech-dispatcher config file"""
|
"""Save the current settings to the speech-dispatcher config file"""
|
||||||
try:
|
try:
|
||||||
for configPath in get_writable_config_targets(self.configTargets):
|
save_speech_settings(
|
||||||
with open(configPath, 'r') as f:
|
SpeechSettings(
|
||||||
content = f.read()
|
rate=self.currentRate,
|
||||||
|
volume=self.currentVolume,
|
||||||
newContent = update_speechd_config_content(
|
pitch=self.currentPitch,
|
||||||
content,
|
),
|
||||||
self.currentRate,
|
self.configTargets,
|
||||||
self.currentVolume,
|
)
|
||||||
self.currentPitch,
|
|
||||||
)
|
|
||||||
|
|
||||||
tempFile = f"/tmp/{os.path.basename(configPath)}.new"
|
|
||||||
with open(tempFile, 'w') as f:
|
|
||||||
f.write(newContent)
|
|
||||||
|
|
||||||
subprocess.run(
|
|
||||||
["sudo", "mv", tempFile, configPath],
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
@@ -230,7 +167,7 @@ class SpeechRateMenu:
|
|||||||
def confirm_saved_settings(self):
|
def confirm_saved_settings(self):
|
||||||
"""Restart speechd, announce success, and wait for confirmation."""
|
"""Restart speechd, announce success, and wait for confirmation."""
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["sudo", "killall", "speech-dispatcher"],
|
["killall", "speech-dispatcher"],
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
time.sleep(1)
|
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.
|
# 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.
|
# 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
|
if [[ $# -ne 2 ]]; then
|
||||||
echo "Usage: $0 \"application name\" \"file name\"."
|
echo "Usage: $0 \"application name\" \"file name\"."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Wait for the application to start
|
# 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
|
sleep 0.05
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -56,7 +54,7 @@ insert_database() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Read so long as the application is running
|
# 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
|
sleep 0.05
|
||||||
text="$(xclip -d "${DISPLAY:-:0}" -selection clipboard -o 2> /dev/null)"
|
text="$(xclip -d "${DISPLAY:-:0}" -selection clipboard -o 2> /dev/null)"
|
||||||
if [[ -f ~/.agmsilent ]]; then
|
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://en.wikipedia.org/wiki/Unicode_equivalence#Combining_and_precomposed_characters
|
||||||
# https://www.effectiveperlprogramming.com/2011/09/normalize-your-perl-source/
|
# 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
|
# Normalize different unicode space characters to the same space
|
||||||
# https://stackoverflow.com/a/43640405
|
# https://stackoverflow.com/a/43640405
|
||||||
alias normalize_spaces="perl -CSDA -plE 's/[^\\S\\t]/ /g'"
|
normalize_spaces() {
|
||||||
alias normalize_unicode="normalize_spaces | nfc"
|
perl -CSDA -plE 's/[^\S\t]/ /g'
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_unicode() {
|
||||||
|
normalize_spaces | nfc
|
||||||
|
}
|
||||||
|
|
||||||
# Normalize text
|
# Normalize text
|
||||||
normalized_text="$(echo "$text" | normalize_unicode)"
|
normalized_text="$(echo "$text" | normalize_unicode)"
|
||||||
|
|||||||
@@ -3,18 +3,18 @@
|
|||||||
# https://bbs.archlinux.org/viewtopic.php?id=117031
|
# https://bbs.archlinux.org/viewtopic.php?id=117031
|
||||||
|
|
||||||
# Wait for the application to start
|
# 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
|
sleep 0.05
|
||||||
done
|
done
|
||||||
|
|
||||||
# Read so long as the application is running
|
# 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
|
sleep 0.05
|
||||||
if [[ -f ~/.agmsilent ]]; then
|
if [[ -f ~/.agmsilent ]]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
wnd_focus=$(xdotool getwindowfocus)
|
wnd_focus=$(xdotool getwindowfocus)
|
||||||
wnd_title=$(xprop -id $wnd_focus WM_NAME)
|
wnd_title=$(xprop -id "$wnd_focus" WM_NAME)
|
||||||
lookfor='"(.*)"'
|
lookfor='"(.*)"'
|
||||||
|
|
||||||
if [[ "$wnd_title" =~ $lookfor ]]; then
|
if [[ "$wnd_title" =~ $lookfor ]]; then
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import threading
|
|||||||
import termios
|
import termios
|
||||||
import tty
|
import tty
|
||||||
import select
|
import select
|
||||||
|
import tempfile
|
||||||
from threading import Timer
|
from threading import Timer
|
||||||
from werkzeug.utils import secure_filename
|
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'."""
|
"""Check if the filename starts with 'voxin-' and ends with '.tgz'."""
|
||||||
return filename.lower().startswith('voxin-') and filename.lower().endswith('.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
|
# Add function to handle voxin installation
|
||||||
def handle_voxin_package(file_path):
|
def handle_voxin_package(file_path):
|
||||||
"""
|
"""
|
||||||
@@ -401,26 +411,18 @@ def handle_voxin_package(file_path):
|
|||||||
|
|
||||||
Returns: dict with status and message
|
Returns: dict with status and message
|
||||||
"""
|
"""
|
||||||
|
extract_dir = None
|
||||||
try:
|
try:
|
||||||
filename = os.path.basename(file_path)
|
filename = os.path.basename(file_path)
|
||||||
logger.info(f"Starting voxin package processing: {filename}")
|
logger.info(f"Starting voxin package processing: {filename}")
|
||||||
extract_dir = os.path.join('/tmp', 'voxin_extract')
|
extract_dir = tempfile.mkdtemp(prefix='voxin_extract_')
|
||||||
|
|
||||||
# Create fresh extraction directory
|
|
||||||
if os.path.exists(extract_dir):
|
|
||||||
shutil.rmtree(extract_dir)
|
|
||||||
os.makedirs(extract_dir)
|
|
||||||
|
|
||||||
# Extract the tarball
|
# Extract the tarball
|
||||||
logger.info(f"Extracting voxin package: {filename}")
|
logger.info(f"Extracting voxin package: {filename}")
|
||||||
subprocess.run(['tar', 'xf', file_path, '-C', extract_dir], check=True)
|
subprocess.run(['tar', 'xf', file_path, '-C', extract_dir], check=True)
|
||||||
|
|
||||||
# Find the installer script
|
# Find the installer script
|
||||||
installer_path = None
|
installer_path = find_voxin_installer(extract_dir)
|
||||||
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
|
|
||||||
|
|
||||||
if not installer_path:
|
if not installer_path:
|
||||||
logger.error(f"Voxin installer script not found in package: {filename}")
|
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
|
# Make the installer executable
|
||||||
os.chmod(installer_path, 0o755)
|
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}")
|
logger.info(f"Running voxin installer for: {filename}")
|
||||||
install_result = subprocess.run(
|
install_result = subprocess.run(
|
||||||
['sudo', installer_path, '-l'],
|
['sudo', str(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'],
|
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=180 # 3 minute timeout
|
timeout=180 # 3 minute timeout
|
||||||
@@ -472,9 +467,6 @@ def handle_voxin_package(file_path):
|
|||||||
text=True
|
text=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Clean up extraction directory
|
|
||||||
shutil.rmtree(extract_dir)
|
|
||||||
|
|
||||||
logger.info(f"Voxin package installation completed successfully: {filename}")
|
logger.info(f"Voxin package installation completed successfully: {filename}")
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -496,6 +488,9 @@ def handle_voxin_package(file_path):
|
|||||||
'message': 'Voice installation error.',
|
'message': 'Voice installation error.',
|
||||||
'details': str(e)
|
'details': str(e)
|
||||||
}
|
}
|
||||||
|
finally:
|
||||||
|
if extract_dir and os.path.exists(extract_dir):
|
||||||
|
shutil.rmtree(extract_dir)
|
||||||
|
|
||||||
@app.route('/classify', methods=['POST'])
|
@app.route('/classify', methods=['POST'])
|
||||||
def classify_files():
|
def classify_files():
|
||||||
|
|||||||
Reference in New Issue
Block a user