From 52e1656e4217c2458fb0e75ae5166ae523d6e1b5 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sat, 12 Jul 2025 13:48:20 -0400 Subject: [PATCH] Initial commit --- home/stormux/.Upheaval | 33 + home/stormux/.Warsim | 14 + home/stormux/.clirc | 113 +++ home/stormux/.inputrc | 6 + home/stormux/.xinitrc | 351 +++++++ home/stormux/.xprofile | 7 + .../system/fenrirscreenreader-tty.service | 15 + usr/lib/systemd/system/shutdown-sound.service | 13 + usr/local/bin/apple_2e.py | 390 ++++++++ usr/local/bin/diagnostics.sh | 33 + usr/local/bin/game_launcher.py | 931 ++++++++++++++++++ usr/local/bin/music_player.py | 621 ++++++++++++ usr/local/bin/ocr.py | 138 +++ usr/local/bin/record.sh | 27 + usr/local/bin/rom_launcher.py | 507 ++++++++++ usr/local/bin/set-voice.py | 358 +++++++ usr/local/bin/speechd_rate.py | 303 ++++++ usr/local/bin/sync_time.sh | 4 + usr/share/sounds/stormux/.gitattributes | 1 + usr/share/sounds/stormux/menu_category.wav | 3 + usr/share/sounds/stormux/menu_move.wav | 3 + usr/share/sounds/stormux/menu_select.wav | 3 + usr/share/sounds/stormux/start.opus | Bin 0 -> 49878 bytes usr/share/sounds/stormux/stop.opus | Bin 0 -> 49309 bytes 24 files changed, 3874 insertions(+) create mode 100644 home/stormux/.Upheaval create mode 100755 home/stormux/.Warsim create mode 100755 home/stormux/.clirc create mode 100644 home/stormux/.inputrc create mode 100755 home/stormux/.xinitrc create mode 100644 home/stormux/.xprofile create mode 100644 usr/lib/systemd/system/fenrirscreenreader-tty.service create mode 100644 usr/lib/systemd/system/shutdown-sound.service create mode 100755 usr/local/bin/apple_2e.py create mode 100755 usr/local/bin/diagnostics.sh create mode 100755 usr/local/bin/game_launcher.py create mode 100755 usr/local/bin/music_player.py create mode 100644 usr/local/bin/ocr.py create mode 100755 usr/local/bin/record.sh create mode 100755 usr/local/bin/rom_launcher.py create mode 100755 usr/local/bin/set-voice.py create mode 100755 usr/local/bin/speechd_rate.py create mode 100755 usr/local/bin/sync_time.sh create mode 100644 usr/share/sounds/stormux/.gitattributes create mode 100644 usr/share/sounds/stormux/menu_category.wav create mode 100644 usr/share/sounds/stormux/menu_move.wav create mode 100644 usr/share/sounds/stormux/menu_select.wav create mode 100644 usr/share/sounds/stormux/start.opus create mode 100644 usr/share/sounds/stormux/stop.opus diff --git a/home/stormux/.Upheaval b/home/stormux/.Upheaval new file mode 100644 index 0000000..dcaefcf --- /dev/null +++ b/home/stormux/.Upheaval @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# Function to clean up on exit +cleanup() { + sudo systemctl stop fenrirscreenreader.service 2>/dev/null || true +} + +# Set up trap for cleanup +trap cleanup EXIT + +if [[ -r "/home/stormux/.local/games/Upheaval/Upheaval_Command_Line" ]]; then + pushd "/home/stormux/.local/games/Upheaval/" > /dev/null + + # Comprehensive terminal reset + reset + clear + stty sane + stty echo + stty icanon + stty -raw + stty -cbreak + + # Start screen reader + sudo systemctl start fenrirscreenreader.service + + # Give screen reader time to initialize + sleep 1 + + # Run the game with proper terminal handling + # Use exec to replace the shell process entirely + exec stdbuf -i0 -o0 -e0 FEXLoader ./Upheaval_Command_Line 2>&1 | grep -v '^vc4: driver missing$' + popd > /dev/null +fi diff --git a/home/stormux/.Warsim b/home/stormux/.Warsim new file mode 100755 index 0000000..e5c3302 --- /dev/null +++ b/home/stormux/.Warsim @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +trap 'sudo systemctl stop fenrirscreenreader.service' EXIT + +export BOX64_LOG=0 +if [[ -r "/home/stormux/.local/games/Warsim/Warsim.exe" ]]; then + pushd "/home/stormux/.local/games/Warsim/" + clear + sudo systemctl start fenrirscreenreader.service + wine Warsim.exe 2> /dev/null | grep -v '^box64' + popd +else + GAME='Warsim' startx +fi diff --git a/home/stormux/.clirc b/home/stormux/.clirc new file mode 100755 index 0000000..ada969b --- /dev/null +++ b/home/stormux/.clirc @@ -0,0 +1,113 @@ +#!/usr/bin/env bash + + +# Stop the screen reader if this closes for any reason +trap 'sudo -n /usr/bin/systemctl stop fenrirscreenreader.service' SIGINT SIGTERM SIGHUP EXIT + +# Start Fenrir for interaction with the terminal +sudo -n /usr/bin/systemctl start fenrirscreenreader.service + +# Clear the screen before loading +clear + +case "$GAME" in + "Alter Aeon") + pushd ~/.local/games/tintin-alteraeon + git pull -q + tt++ aa.tin + popd + ;; + "BPG") + pushd ~/.local/games/bpg + git pull -q + ./bpg.sh + popd + ;; + "Empire MUD") + pushd ~/.local/games/tintin-empiremud + git pull -q + tt++ em.tin + popd + ;; + "End of Time") + pushd ~/.local/games/tintin-endoftime + git pull -q + tt++ eot.tin + popd + ;; + "Kallisti MUD") + pushd ~/.local/games/tintin-kallisti-pack + git pull -q + tt++ kallisti.tin + popd + ;; + "RS Games") + pushd ~/.local/games/tintin-rsgames + git pull -q + tt++ rs.tin + popd + ;; + "Slay the Text") + pushd ~/.local/games/slaythetext + git pull -q + python3 main.py + popd + ;; + "Stationfall") + frotz ~/.local/frotz/Stationfall/stationfall-r107-s870430.z3 + ;; + "Planetfall") + frotz ~/.local/frotz/Planetfall/planetfall-r39-s880501.z3 + ;; + "The Hitchhiker's Guide to the Galaxy") + frotz ~/.local/frotz/"The Hitchhiker's Guide to the Galaxy/HITCHHIK.DAT" + ;; + "Zork 1") ;& + "Zork 2") ;& + "Zork 3") + frotz ~/.local/frotz/${GAME/ /}/DATA/ZORK${GAME##* }.DAT + ;; + "IRC") + if ! ping -c1 stormux.org &> /dev/null ; then + spd-say -Cw "No internet connection detected. Please go to the system menu and select configure internet." + exit 0 + fi + configFile="$HOME/.irssi/config" + validNickRegex='^[[:alnum:]_-]*$' + # Check if nick and user_name are still set to "stormux" + if grep -qE '^\s*nick = "stormux";' "$configFile" && grep -qE '^\s*user_name = "stormux";' "$configFile"; then + echo "First time connection setup:" + echo + echo "Let's get you set up with your own nick." + echo "This is the name other people will see when you join, chat, leave, etc." + echo + while : ; do + echo "Enter your desired IRC nick:" + read -re newNick + if [[ $newNick =~ $validNickRegex ]]; then + # Update nick and user_name in config + sed -i "s/^\(\s*nick = \)\"stormux\";/\1\"$newNick\";/" "$configFile" + sed -i "s/^\(\s*user_name = \)\"stormux\";/\1\"$newNick\";/" "$configFile" + echo "Configuration updated. Launching irssi..." + break + else + echo "Invalid IRC nickname. Must start with a letter and contain only letters, numbers, and these symbols: - [ ] \\ \` ^ { } _" + fi + done + fi + # Launch irssi + /usr/bin/irssi + ;; + "Network Configuration") + count=0 + while [[ $count -lt 10 ]]; do + sleep 1 + if echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock 2> /dev/null ; then + break + fi + done + nmtui-connect + ;; + *".md") /usr/bin/markdown -toc "$GAME" | /usr/bin/w3m -T text/html ;; + "/usr/bin/"*) $GAME ;; +esac diff --git a/home/stormux/.inputrc b/home/stormux/.inputrc new file mode 100644 index 0000000..0dcf6ea --- /dev/null +++ b/home/stormux/.inputrc @@ -0,0 +1,6 @@ +# Reload changes with control+x followed by control+r +set echo-control-characters off + +# History searching with up and down arrows. +"\e[A": history-search-backward +"\e[B": history-search-forward diff --git a/home/stormux/.xinitrc b/home/stormux/.xinitrc new file mode 100755 index 0000000..e773dbb --- /dev/null +++ b/home/stormux/.xinitrc @@ -0,0 +1,351 @@ +#!/usr/bin/env bash + +# Function to run a game with FEXLoader +fex_load() { + local gameDir="$1" + local exeName="$2" + export FEX_NO_SANDBOX=1 + export MESA_GL_VERSION_OVERRIDE=3.2 + export SPD_SOCKET=/run/user/1000/speech-dispatcher/speechd.sock + pushd "$HOME/.local/games/$gameDir" + FEX_NO_SANDBOX=1 MESA_GL_VERSION_OVERRIDE=3.2 BOX64_DYNAREC_STRONGMEM=1 DISPLAY=:0 FEXLoader ./$exeName + popd +} + +# Function to run a Windows game with wine via FEXBash +run_wine() { + local gameDir="$1" + local exeName="$2" + local needNvda="${3:-}" + pushd "$HOME/.local/games/$gameDir" + if [[ ${#needNvda} > 1 ]]; then + systemctl --user start ${needNvda}.service + FEXBash -c "wine \"$exeName\"" + systemctl --user stop ${needNvda}.service + else + FEXBash -c "wine $exeName" + fi + popd +} + +# Function to handle downloadable games with installation +setup_and_run_downloadable() { + local zipName="$1" + local gameDir="$2" + local executable="$3" + local fallbackUrl="$4" + local needNvda="$5" + local zipPath=~/Downloads/"$zipName" + local extractDir="$HOME/.local/games/$gameDir" + if [[ -e "$zipPath" ]]; then + # Recreate extractDir + rm -rf "$extractDir" + mkdir -p "$extractDir" + # Get the first path component of each file in the zip + mapfile -t topDirs < <(unzip -l "$zipPath" | awk 'NR>3 {print $4}' | grep '/$' | cut -d/ -f1 | sort -u | grep -v '^$') + if [[ ${#topDirs[@]} -eq 1 ]]; then + # Zip has a single top-level dir Ăse it as final dir + unzip -q "$zipPath" -d ~/.local/games + else + # Unzips file into the given directory without creating a subdirectory + unzip -q "$zipPath" -d "$extractDir" + fi + # Copy NVDA DLLs + for i in 32 64; do + find "$extractDir" -type f -name "nvdaControllerClient${i}.dll" -exec cp -v "$HOME/.local/games/nvda/nvdaControllerClient${i}.dll" '{}' \; + done + rm -f "$zipPath" + fi + if [[ -e "$extractDir/$executable" ]]; then + run_wine "${extractDir##*/}" "./$executable" "$needNvda" + else + run_web "$fallbackUrl" + fi +} + +# Function to handle all web-based games +run_web() { + orca & + exec brave "$1" +} + +# Function to handle unpacked web games +# Function to handle unpacked web games +setup_and_run_web_game() { + local zipName="$1" + local gameDir="$2" + local htmlPath="$3" + local fallbackUrl="$4" + local zipPath=~/Downloads/"$zipName" + local extractDir="$HOME/.local/games/$gameDir" + if [[ -e "$zipPath" ]]; then + # Recreate extractDir + rm -rf "$extractDir" + mkdir -p "$extractDir" + # Get the first path component of each file in the zip + mapfile -t topDirs < <(unzip -l "$zipPath" | awk 'NR>3 {print $4}' | grep '/$' | cut -d/ -f1 | sort -u | grep -v '^$') + if [[ ${#topDirs[@]} -eq 1 ]]; then + # Zip has a single top-level dir - extract to ~/.local/games + unzip -q "$zipPath" -d "$HOME/.local/games" + else + # Unzips file into the given directory + unzip -q "$zipPath" -d "$extractDir" + fi + rm -f "$zipPath" + fi + if [[ -e "$extractDir/$htmlPath" ]]; then + run_web "$extractDir/$htmlPath" + else + run_web "$fallbackUrl" + fi +} + +# Function to run native applications +run_native() { + local gameDir="$1" + local exeName="$2" + pushd "/home/stormux/.local/games/$gameDir" + case "${exeName##*.}" in + "py") + python3 "$exeName" + ;; + "py") + ./"$exeName" + ;; + esac + popd +} + +# Initial setup +if [[ -e /etc/X11/xorg.conf.d/10-headless.conf ]]; then + export LIBGL_ALWAYS_SOFTWARE=1 +fi + +[ -f /etc/xprofile ] && . /etc/xprofile +[ -f ~/.xprofile ] && . ~/.xprofile +export BOX64_PATH="$HOME/.fex-emu/RootFS/ArchLinux/usr/bin" +export BOX64_NOBANNER=1 +export MESA_GL_VERSION_OVERRIDE=3.2 +export BOX64_DYNAREC_STRONGMEM=1 +export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus +export DBUS_SESSION_BUS_PID +exec dwm & +xbindkeys + + +# Main game launcher +case "$GAME" in + "Apple 2e") + exec mame apple2ee -sl1 echoii -flop1 ~/.local/games/apple2e/Echo\ II\ -\ Textalker\ DOS\ 3.3.dsk -samplerate 48000 -sound pulse + ;; + "Audio Disc") + run_wine "audiodisc" "disco.exe" + ;; + "BallBouncer") + fex_load BallBouncer BallBouncer + ;; + "Battle of the Hunter") + run_wine "Battle of the Hunter" "bth.exe" "nvda2speechd" + ;; + "blueman-manager") + orca & + blueman-manager + ;; + "Bokurano Daibouken") + /home/stormux/.local/files/clipboard_translator.sh play.exe bokurano-daibouken & + run_wine "bk" "play.exe" + killall speech-dispatcher + ;; + "Bokurano Daibouken 2") + /home/stormux/.local/files/clipboard_translator.sh play.exe bokurano-daibouken2 & + run_wine "bk2" "play.exe" + killall speech-dispatcher + ;; + "Bokurano Daibouken 3") + if [[ -d /home/stormux/.local/games/bk3/dict ]] || [[ -e ~/Downloads/dict.dat ]]; then + [[ -e ~/Downloads/dict.dat ]] && mv ~/Downloads/dict.dat /home/stormux/.local/games/bk3/ + run_wine "bk3" "play.exe" "nvda2speechd" + else + /home/stormux/.local/files/clipboard_translator.sh play.exe bokurano-daibouken3 & + run_wine "bk3" "play.exe" + fi + killall speech-dispatcher + ;; + "Bop It") + ~/.local/files/speak_window_title.sh bop.exe & + run_wine "BopIt" "bop.exe" + ;; + "Brave") + run_web "" # Runs brave without a specific URL + ;; + "Challenge of the Horse") + run_wine "Challenge of the Horse" "game.exe" "nvda2speechd" + ;; + "Clashes of the Sky") + run_wine "clashes_of_the_sky" "clash.exe" "nvda2speechd" + ;; + "Constant Motion") + run_wine "ConstantMotion" "cm.exe" "nvda2speechd" + ;; + "Crazy Party") + run_wine "Crazy-Party-beta82" "Crazy\ Party.exe" "nvda2speechd-important" + ;; + "Doom") + pushd /home/stormux/.local/games/doom/toby-doom-launcher + git pull -q + orca & + exec "./Toby Doom Launcher.py" + popd + ;; + "Dosbox") + exec dosbox-x -fastlaunch -conf /opt/talking-dosbox/dosbox-x.conf + ;; + "Fantasy Story 2") + pushd "/home/stormux/.local/games/Fantasy Story 2/FS2_3.0_Linux" + FEXBash './fs2.x86_64' + popd + ;; + "Golf") + run_wine "golf" "golf.exe" + ;; + "Haunted House") + pushd /home/stormux/.local/games/haunted-house + ./runner haunted-house + popd + ;; + "Haunted Party") + setup_and_run_downloadable "hp.zip" "hp" "hp.exe" "https://tunmi13.itch.io/haunted-party" "nvda2speechd" + ;; + "Kaskade") + setup_and_run_downloadable "battlefield-2d-win.zip" "bf" "bf.exe" "https://tunmi13.itch.io/kaskade" "nvda2speechd" + ;; + "Mach1") + run_wine "mach1" "mach1.exe" + ;; + "Manamon 2") + run_wine "Manamon 2" "rpg.exe" "nvda2speechd" + ;; + "Mine Racer") + pushd /home/stormux/.local/games/Mineracer + ./mineracer + popd + ;; + "Oh Shit") + run_wine "oh_shit" "OhShit.exe" "nvda2speechd" + ;; + "Pong") + run_wine "pong" "pong.exe" + ;; + "Q9 Action Game") + run_wine "Q9 Action Game" "q9.exe" + ;; + "Retro Arch") + /usr/bin/retroarch --accessibility + killall speech-dispatcher + ;; + "River Raiders") + run_wine "River Raiders" "raid.exe" + ;; + "RS Games") + run_wine "RS Games Client" "rsg.exe" "nvda2speechd-important" + ;; + "Scramble") + run_wine "scramble_win32" "scramble.exe" "nvda2speechd" + ;; + "Screaming Strike 2") + run_wine "Screaming Strike 2" "play.exe" "nvda2speechd" + ;; + "Scrolling Battles") + run_wine "Scrolling Battles" "sb.exe" "nvda2speechd" + ;; + "Shadow Line") + run_wine "ShadowRine_FullVoice" "play_sr.exe" "nvda2speechd" + ;; + "Shooter") + run_wine "shooter" "Shooter.exe" "nvda2speechd" + ;; + "Side Party") + setup_and_run_downloadable "sideparty-win.zip" "SideParty" "SideParty.exe" "https://masonasons.itch.io/sideparty" "nvda2speechd" + ;; + "Skateboarder Pro") + run_wine "sb_pro_rw" "sb_pro.exe" "nvda2speechd" + ;; + "Sketchbook (Your World)") + run_wine "SBYW" "SBYW.exe" "nvda2speechd-important" + ;; + "SoundRTS") + run_native "SoundRTS" "soundrts.py" + ;; + "Super Egg Hunt") + run_wine "super egg hunt" "superegghunt.exe" + ;; + "Super Liam") + run_wine "Super Liam" "sl.exe" + ;; + "The Blind Swordsman") + FEXBash -c 'wine ~/.local/games/TheBlindSwordsman.exe' + ;; + "The Tornado Chicken") + fex_load "The Tornado Chicken" "Ttc" + ;; + "Top Speed 3") + run_wine "Top Speed 3" "TopSpeed.exe" + ;; + "Toy Mania") + if [[ -e ~/Downloads/ToyMania_windows_portable_password_is_GrateCollector.7z ]]; then + rm -rf ~/.local/games/ToyMania + mkdir -p ~/.local/games/ToyMania + 7z x ~/Downloads/ToyMania_windows_portable_password_is_GrateCollector.7z -o/home/stormux/.local/games/ToyMania -pGrateCollector + rm -f ~/Downloads/ToyMania_windows_portable_password_is_GrateCollector.7z + fi + if [[ -e ~/.local/games/ToyMania/tm.exe ]]; then + cp ~/.local/games/nvda/nvdaControllerClient64.dll ~/.local/games/ToyMania/lib/nvdaControllerClient64.dll + systemctl --user start nvda2speechd-important.service + pushd ~/.local/games/ToyMania + exec FEXBash -c 'wine ./tm.exe' + popd + systemctl --user stop nvda2speechd-important.service + else + orca & + exec brave "https://tsatria03.itch.io/toymania" + fi + ;; + "Upheaval") + setup_and_run_downloadable "upheaval-linux-console.zip" "Upheaval" "Upheaval_Command_Line" "https://leonegaming.itch.io/upheaval" + ;; + "Villains From Beyond") + run_wine "villains from beyond" "game.exe" + ;; + "Warsim") + setup_and_run_downloadable "Warsim Full Game.zip" "Warsim" "Warsim.exe" "https://huw2k8.itch.io/warsim" + ;; + "Wheels of Prio") + fex_load "Wheels of Prio" "Wp" + ;; + "Wicked Quest") + run_native "wicked-quest" "wicked_quest.py" + ;; + "Zombowl") + run_native "zombowl" "zombowl.py" + ;; + "https://shiftbacktick.itch.io/periphery-synthetic-ep") + setup_and_run_web_game "periphery-synthetic-ep-html5.zip" "periphery-synthetic-ep" "index.html" "$GAME" + ;; + "https://"*) + run_web "$GAME" + ;; + *".dsk") + xbindkeys + exec mame apple2ee -sl1 echoii -flop1 ~/.local/games/apple2e/Echo\ II\ -\ Textalker\ DOS\ 3.3.dsk -flop2 "$GAME" -samplerate 48000 -sound pulse + ;; + *"/Roms/"*) + exec mednafen -sounddriver sdl -sound 1 "$GAME" + ;; + *) + echo "No valid game specified in \$GAME" + sleep 5 + ;; +esac + +# Make sure X goes down after game ends +pkill -15 Xorg diff --git a/home/stormux/.xprofile b/home/stormux/.xprofile new file mode 100644 index 0000000..4aa3523 --- /dev/null +++ b/home/stormux/.xprofile @@ -0,0 +1,7 @@ +# Accessibility variables +export ACCESSIBILITY_ENABLED=1 +export GTK_MODULES=gail:atk-bridge +export GNOME_ACCESSIBILITY=1 +export QT_ACCESSIBILITY=1 +export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1 +export SAL_USE_VCLPLUGIN=gtk3 diff --git a/usr/lib/systemd/system/fenrirscreenreader-tty.service b/usr/lib/systemd/system/fenrirscreenreader-tty.service new file mode 100644 index 0000000..6307648 --- /dev/null +++ b/usr/lib/systemd/system/fenrirscreenreader-tty.service @@ -0,0 +1,15 @@ +[Unit] +Description=Fenrir screenreader +Wants=systemd-udev-settle.service +After=systemd-udev-settle.service getty.target +[Service] +Type=forking +PIDFile=/var/run/fenrir.pid +ExecStart=/usr/bin/fenrir -i 1 +ExecReload=/usr/bin/kill -HUP $MAINPID +Restart=always +#Group=fenrirscreenreader +#User=fenrirscreenreader + +[Install] +WantedBy=getty.target diff --git a/usr/lib/systemd/system/shutdown-sound.service b/usr/lib/systemd/system/shutdown-sound.service new file mode 100644 index 0000000..3e54a71 --- /dev/null +++ b/usr/lib/systemd/system/shutdown-sound.service @@ -0,0 +1,13 @@ +[Unit] +Description=Play shutdown sound +DefaultDependencies=no +Before=shutdown.target reboot.target halt.target +After=multi-user.target pipewire.service pipewire.socket sound.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/play /usr/share/sounds/stormux/stop.opus +#ExecStartPost=/bin/sleep 3.5 + +[Install] +WantedBy=halt.target reboot.target shutdown.target diff --git a/usr/local/bin/apple_2e.py b/usr/local/bin/apple_2e.py new file mode 100755 index 0000000..7e9c93d --- /dev/null +++ b/usr/local/bin/apple_2e.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Self-voiced Terminal Menu for Apple IIe Disk Launcher + +import os +import sys +import time +import curses +import speechd # Python bindings for Speech Dispatcher +import configparser + +class VoicedDiskMenu: + def __init__(self, title="Apple 2e Disk Menu"): + self.title = title + self.menu_items = [] # List to store (name, path) tuples + self.current_index = 0 # Index of current selection + 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) + + # Load settings + self.load_settings() + + # Initialize speech client + self.speech_client = None + self.init_speech() + + def init_speech(self): + """Initialize the speech client""" + try: + self.speech_client = speechd.SSIPClient("apple_menu") + self.speech_client.set_priority(speechd.Priority.IMPORTANT) + self.speech_client.set_punctuation(speechd.PunctuationMode.SOME) + + # Apply speech rate from settings + self.speech_client.set_rate(self.speech_rate) + except Exception as e: + print(f"Could not initialize speech: {e}") + # 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 + + 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}") + + def increase_speech_rate(self): + """Increase speech rate""" + self.speech_rate = min(100, self.speech_rate + 10) # Max is 100 + if self.speech_client: + try: + self.speech_client.set_rate(self.speech_rate) + self.speak(f"Speech rate: {self.speech_rate}") + except Exception as e: + print(f"Error adjusting speech rate: {e}") + + # Save the new setting + self.save_settings() + + def decrease_speech_rate(self): + """Decrease speech rate""" + self.speech_rate = max(-100, self.speech_rate - 10) # Min is -100 + if self.speech_client: + try: + self.speech_client.set_rate(self.speech_rate) + self.speak(f"Speech rate: {self.speech_rate}") + except Exception as e: + print(f"Error adjusting speech rate: {e}") + + # Save the new setting + self.save_settings() + + def speak(self, text, interrupt=True): + """Speak the given text with option to interrupt existing speech""" + if self.speech_client is None: + return + + try: + if interrupt: + self.stop_speech() + + self.speech_client.speak(text) + except Exception as e: + # If speech fails, try to reinitialize and try once more + try: + self.init_speech() + if self.speech_client: + self.speech_client.speak(text) + except: + # If reinitializing fails, just give up silently + pass + + def stop_speech(self): + """Stop any ongoing speech""" + if self.speech_client is None: + return + + try: + self.speech_client.cancel() + except Exception as e: + # If cancel fails, try to reinitialize + self.init_speech() + + def announce_current_item(self, interrupt=True): + """Announce the currently selected menu item""" + if self.menu_items and 0 <= self.current_index < len(self.menu_items): + name = self.menu_items[self.current_index][0] + self.speak(name, interrupt=interrupt) + + def execute_current_item(self): + """Execute the currently selected menu item""" + if self.menu_items and 0 <= self.current_index < len(self.menu_items): + _, path = self.menu_items[self.current_index] + + # Clean up resources before executing the command + self.cleanup(full_cleanup=True) + + # Execute the command to launch the disk file + os.system(f'export GAME="{path}" && startx') + sys.exit(0) # Exit after launching + + def speak_help(self): + """Speak help information""" + helpText = """ + Navigation controls: + Up arrow: Previous disk. + Down arrow: Next disk. + Enter: Launch selected disk. + H key: Hear these instructions again. + Left bracket: Decrease speech rate. + Right bracket: Increase speech rate. + Escape or Q: Exit the menu. + Any key will interrupt speech. + """ + self.speak(helpText) + + 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: Navigate | Enter: Select | H: Help | [ ] Rate | Q/Esc: Quit" + x = max(0, w // 2 - len(helpText) // 2) + self.stdscr.addstr(3, x, helpText) + + # Check if we have items + if not self.menu_items: + message = "No disk files found." + x = max(0, w // 2 - len(message) // 2) + self.stdscr.addstr(5, x, message, curses.A_DIM) + else: + # Show a limited number of items, centered around the current selection + max_display = min(h - 7, len(self.menu_items)) # Max number of items to display + + # Calculate starting index for display + half_display = max_display // 2 + if self.current_index < half_display: + start_idx = 0 + elif self.current_index >= len(self.menu_items) - half_display: + start_idx = max(0, len(self.menu_items) - max_display) + else: + start_idx = self.current_index - half_display + + # Draw visible menu items + for i in range(start_idx, min(start_idx + max_display, len(self.menu_items))): + y = (i - start_idx) + 5 # Start items at line 5 + + # Highlight the selected item + name = self.menu_items[i][0] + if i == self.current_index: + text = f" > {name} " + attr = curses.A_REVERSE + else: + text = f" {name} " + attr = curses.A_NORMAL + + x = max(0, w // 2 - len(text) // 2) + self.stdscr.addstr(y, x, text, attr) + + # Draw speech rate indicator + rateText = f"Speech Rate: {self.speech_rate}" + self.stdscr.addstr(h-2, 2, rateText) + + self.stdscr.refresh() + + def cleanup(self, full_cleanup=False): + """Clean up resources before exiting or executing a command + + Args: + full_cleanup: If True, also close curses. Used when exiting or running a command. + """ + # Stop any speech + self.stop_speech() + + # Close speech client + if self.speech_client: + try: + self.speech_client.close() + except: + pass + self.speech_client = None + + # Restore terminal settings if curses was initialized + if full_cleanup and self.curses_initialized: + 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 load_disks_from_directory(self, directory_path): + """Load disk files from the specified directory""" + # Expand the path (in case it contains ~) + directory_path = os.path.expanduser(directory_path) + + # Check if directory exists + if not os.path.exists(directory_path) or not os.path.isdir(directory_path): + print(f"Directory {directory_path} does not exist or is not a directory") + return + + try: + # Get all .dsk files in the directory + files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f)) + and f.lower().endswith(('.dsk', '.do'))] + + # Sort files alphabetically + files.sort() + + # Create menu items + for file in files: + # Get full path to the file + file_path = os.path.join(directory_path, file) + + # Create display name - remove extension + display_name = os.path.splitext(file)[0] + + # Replace underscores with spaces for better readability + display_name = display_name.replace('_', ' ') + + # Add to menu items list + self.menu_items.append((display_name, file_path)) + + except Exception as e: + print(f"Error loading disk directory: {e}") + + def run(self): + """Run the menu system""" + # Check if menu is empty + if not self.menu_items: + message = "No disk files found. Exiting." + print(message) + + # Speak the message + self.init_speech() + if self.speech_client: + self.speak(message) + # Wait for speech to finish (rough estimate) + time.sleep(3) + + # Clean up and exit properly + self.cleanup(full_cleanup=True) + sys.exit(0) + + try: + # Initialize curses + self.stdscr = curses.initscr() + self.curses_initialized = True + curses.noecho() + curses.cbreak() + self.stdscr.keypad(True) + + # Initial draw + self.draw_menu() + + # Welcome message + self.speak(self.title) + + # Wait for initial speech to finish before announcing first item + time.sleep(1) + self.announce_current_item(interrupt=False) + + # 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: + # Move to previous item + self.current_index = (self.current_index - 1) % len(self.menu_items) + self.draw_menu() + self.announce_current_item() + + elif key == curses.KEY_DOWN: + # Move to next item + self.current_index = (self.current_index + 1) % len(self.menu_items) + self.draw_menu() + self.announce_current_item() + + elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key + self.execute_current_item() + + elif key == ord('h') or key == ord('H'): # Help + self.speak_help() + + elif key == ord('['): # Decrease speech rate + self.decrease_speech_rate() + self.draw_menu() + + elif key == ord(']'): # Increase speech rate + self.increase_speech_rate() + self.draw_menu() + + elif key == 27 or key == ord('q') or key == ord('Q'): # Esc or Q + break + + except Exception as e: + # End curses in case of error + if self.curses_initialized: + try: + curses.endwin() + except: + pass + print(f"An error occurred: {e}") + finally: + # Clean up - safe to call even if curses wasn't initialized + self.cleanup(full_cleanup=True) + + +# Run the menu +if __name__ == "__main__": + # Create the menu + menu = VoicedDiskMenu() + + # Load disk files from the Apple IIe disks directory + menu.load_disks_from_directory("~/.local/games/apple2e/disks/") + + # Run the menu + menu.run() diff --git a/usr/local/bin/diagnostics.sh b/usr/local/bin/diagnostics.sh new file mode 100755 index 0000000..d7eb075 --- /dev/null +++ b/usr/local/bin/diagnostics.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +check_usb_port() { + usb_path=$(udevadm info --query=path --name="$device") + # Check the bus this USB device is on + bus_num=$(echo "$usb_path" | grep -o 'usb[0-9]*' | grep -o '[0-9]*' | head -n1) + # Read the speed from sysfs + speed_file="/sys/bus/usb/devices/usb${bus_num}/speed" + if [[ -f "$speed_file" ]]; then + speed=$(cat "$speed_file") + if [[ $speed -lt 1000 ]]; then + spd-say -Cw "Warning. USB 2.0 detected. Please power off the Pi and move the USB drive to a USB 3 port for better performance." + fi + fi +} + + +# set performance +echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor + +# Find the USB bus speed for the boot drive +device=$(findmnt / -o SOURCE -n | sed 's/[0-9]*$//') +if [[ "$device" == /dev/sd* ]]; then + check_usb_port +fi + +# File system trim +rootPartition=$(findmnt / -o SOURCE -n) +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 +fi diff --git a/usr/local/bin/game_launcher.py b/usr/local/bin/game_launcher.py new file mode 100755 index 0000000..1695b90 --- /dev/null +++ b/usr/local/bin/game_launcher.py @@ -0,0 +1,931 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Self-voiced Terminal Menu System for Stormux game image + +import os +import sys +import time +import threading +import signal +import curses +import subprocess +import speechd # Python bindings for Speech Dispatcher +import configparser +import pathlib +import re +import simpleaudio as sa + +class VoicedMenu: + def __init__(self, title="Stormux Game Menu"): + self.title = title + self.menuSections = {} # Dictionary to hold sections and their items + self.sectionNames = [] # List to maintain section order + self.currentSection = 0 # Index of current section + self.currentItemIndices = {} # Current item index for each section + self.stdscr = None + + # System services dictionary - maps friendly names to service names + self.systemMenuServices = { + 'Braille': 'brltty.path', + 'D L N A Server': 'minidlna.service', + 'Fenrir Screen Reader': 'fenrirscreenreader-tty.service', + 'Bluetooth': 'bluetooth.service', + 'SSH': 'sshd.service' + } + + # 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) + self.volume = 50 # Default volume level + + # Load settings + self.load_settings() + + # Initialize speech client + self.speechClient = None + self.init_speech() + + # Track playing sound + self.currentSound = None + + def init_speech(self): + """Initialize the speech client""" + try: + self.speechClient = speechd.SSIPClient('stormux_menu') + self.speechClient.set_priority(speechd.Priority.IMPORTANT) + self.speechClient.set_punctuation(speechd.PunctuationMode.SOME) + + # Apply speech rate from settings + self.speechClient.set_rate(self.speechRate) + except Exception as e: + print(f"Could not initialize speech: {e}") + # 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) + + # Load volume settings + if 'Volume' in self.config: + self.volume = self.config.getint('Volume', 'level', fallback=50) + except Exception as e: + print(f"Error loading settings: {e}") + # If loading fails, we'll use default values + + 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) + + # Save volume settings + if 'Volume' not in self.config: + self.config['Volume'] = {} + + self.config['Volume']['level'] = str(self.volume) + + # 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): + """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}") + except Exception as e: + print(f"Error adjusting speech rate: {e}") + + # Save the new setting + self.save_settings() + + def decrease_speech_rate(self): + """Decrease speech rate""" + self.speechRate = max(-100, self.speechRate - 10) # Min is -100 + if self.speechClient: + try: + self.speechClient.set_rate(self.speechRate) + self.speak(f"Speech rate: {self.speechRate}") + except Exception as e: + print(f"Error adjusting speech rate: {e}") + + # Save the new setting + self.save_settings() + + def get_current_volume(self): + """Get the current system volume percentage""" + try: + result = subprocess.run( + ['pactl', 'get-sink-volume', '@DEFAULT_SINK@'], + capture_output=True, text=True, check=True + ) + output = result.stdout + # Extract percentage from output like "Volume: front-left: 27111 / 41% / -23.00 dB" + match = re.search(r'(\d+)%', output) + if match: + return int(match.group(1)) + return self.volume # Return saved volume if parsing fails + except Exception as e: + print(f"Error getting volume: {e}") + return self.volume # Return saved volume if command fails + + def set_volume(self, volumePercent): + """Set the system volume to the specified percentage""" + # Ensure volume is between 0 and 150% + volumePercent = max(0, min(150, volumePercent)) + + try: + subprocess.run( + ['pactl', 'set-sink-volume', '@DEFAULT_SINK@', f'{volumePercent}%'], + check=True + ) + self.volume = volumePercent + self.save_settings() + return True + except Exception as e: + print(f"Error setting volume: {e}") + return False + + def increase_volume(self): + """Increase the system volume by 5%""" + currentVolume = self.get_current_volume() + newVolume = min(150, currentVolume + 5) # Max 150% + if self.set_volume(newVolume): + self.speak(f"Volume {newVolume} percent") + self.draw_menu() + + def decrease_volume(self): + """Decrease the system volume by 5%""" + currentVolume = self.get_current_volume() + newVolume = max(0, currentVolume - 5) # Min 0% + if self.set_volume(newVolume): + self.speak(f"Volume {newVolume} percent") + self.draw_menu() + + def play_sound(self, soundName): + """Play a sound effect using simpleaudio with ability to cancel previous sounds""" + try: + # Cancel any currently playing sound + self.stop_sound() + + soundsDir = "/usr/share/sounds/stormux" + soundFile = os.path.join(soundsDir, f"{soundName}.wav") + + # Check if the file exists + if not os.path.exists(soundFile): + print(f"Sound file not found: {soundFile}") + return False + + # Play the sound and store the play_obj + waveObj = sa.WaveObject.from_wave_file(soundFile) + self.currentSound = waveObj.play() + return True + except Exception as e: + print(f"Error playing sound {soundName}: {e}") + return False + + def stop_sound(self): + """Stop any currently playing sound""" + if self.currentSound is not None and self.currentSound.is_playing(): + self.currentSound.stop() + self.currentSound = None + + def check_service_status(self, serviceName): + """Check if a system service is active""" + try: + result = subprocess.run( + ['systemctl', 'is-active', serviceName], + capture_output=True, text=True + ) + return result.stdout.strip() == 'active' + except Exception as e: + print(f"Error checking {serviceName} status: {e}") + return False + + def toggle_service(self, friendlyName): + """Toggle a system service on/off""" + # Get the actual service name from the dictionary + serviceName = self.systemMenuServices.get(friendlyName) + if not serviceName: + print(f"Unknown service: {friendlyName}") + return + + isActive = self.check_service_status(serviceName) + + # Clean up curses before running the command with sudo + curses.endwin() + + # Clean up speech client + if self.speechClient: + self.speechClient.close() + self.speechClient = None + + # Execute the appropriate command based on current status + action = "" + if serviceName not in ["fenrirscreenreader-tty.service", "minidlna.service"]: + action = "disable" if isActive else "enable" + else: + action = "stop" if isActive else "start" + + command = f"sudo systemctl {action} {serviceName} --now" + + try: + os.system(command) + print(f"{friendlyName} {action}d successfully") + except Exception as e: + print(f"Error {action}ing {friendlyName}: {e}") + + # Restart speech client + self.init_speech() + + # Restart curses + self.stdscr = curses.initscr() + curses.noecho() + curses.cbreak() + self.stdscr.keypad(True) + + # Announce the action + self.speak(f"{friendlyName} {action}d") + + # Update the menu with the new service status + self.update_service_menu_items() + + # Update Bluetooth menu items if Bluetooth service was toggled + if friendlyName == "Bluetooth": + self.update_bluetooth_menu_items() + + # Redraw the menu + self.draw_menu() + + def toggle_screen(self, screenType): + """Toggle between HDMI screen and headless mode""" + # Remove any existing configuration + try: + os.system("sudo rm /etc/X11/xorg.conf.d/10-*.conf") + + # Copy the appropriate configuration file + if screenType == "headless": + configFile = "/home/stormux/.local/files/10-headless.conf" + message = "HDMI Screen disabled." + else: + configFile = "/home/stormux/.local/files/10-screen.conf" + message = "HDMI Screen enabled." + + # Copy the configuration file + os.system(f"sudo cp {configFile} /etc/X11/xorg.conf.d/") + + self.speak(message, interrupt=False) + + except Exception as e: + message = f"Error changing screen configuration: {e}" + print(message) + self.speak(message) + + def update_service_menu_items(self): + """Update all service-related menu items based on their current status""" + # Remove any existing service items + if "System" in self.menuSections: + # Find and remove any service-related items + self.menuSections["System"] = [item for item in self.menuSections["System"] + if not any(service in item[0] for service in self.systemMenuServices.keys())] + + # Add the appropriate items based on current status for each service + for friendlyName, serviceName in self.systemMenuServices.items(): + isActive = self.check_service_status(serviceName) + if isActive: + self.add_item("System", f"Disable {friendlyName}", + lambda fn=friendlyName: self.toggle_service(fn)) + else: + self.add_item("System", f"Enable {friendlyName}", + lambda fn=friendlyName: self.toggle_service(fn)) + + def update_bluetooth_menu_items(self): + """Update Bluetooth-related menu items in Accessories section""" + if "Accessories" in self.menuSections: + # Remove any existing Bluetooth-related items + self.menuSections["Accessories"] = [item for item in self.menuSections["Accessories"] + if "Bluetooth" not in item[0]] + + # Add Bluetooth management item only if Bluetooth is enabled + if self.check_service_status('bluetooth.service'): + self.add_item("Accessories", "Manage Bluetooth Devices", + "GAME=blueman-manager startx") + + def add_section(self, sectionName): + """Add a new section to the menu""" + if sectionName not in self.menuSections: + self.menuSections[sectionName] = [] + self.sectionNames.append(sectionName) + self.currentItemIndices[sectionName] = 0 + + def add_item(self, sectionName, name, command): + """Add a menu item to a specific section""" + # Create section if it doesn't exist + if sectionName not in self.menuSections: + self.add_section(sectionName) + + self.menuSections[sectionName].append((name, command)) + + 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 as e: + # 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 as e: + # If cancel fails, try to reinitialize + self.init_speech() + + def get_current_items(self): + """Get items from the current section""" + currentSectionName = self.sectionNames[self.currentSection] + return self.menuSections[currentSectionName] + + def get_current_item_index(self): + """Get the current item index in the current section""" + currentSectionName = self.sectionNames[self.currentSection] + return self.currentItemIndices[currentSectionName] + + def set_current_item_index(self, index): + """Set the current item index for the current section""" + currentSectionName = self.sectionNames[self.currentSection] + self.currentItemIndices[currentSectionName] = index + + def announce_current_section(self, interrupt=True): + """Announce the currently selected section""" + if 0 <= self.currentSection < len(self.sectionNames): + sectionName = self.sectionNames[self.currentSection] + self.speak(sectionName, interrupt=interrupt) + + def announce_current_item(self, interrupt=True): + """Announce the currently selected menu item""" + if len(self.sectionNames) > 0: + items = self.get_current_items() + index = self.get_current_item_index() + + if 0 <= index < len(items): + name = items[index][0] + self.speak(name, interrupt=interrupt) + + def execute_current_item(self): + """Execute the currently selected menu item""" + if len(self.sectionNames) > 0: + items = self.get_current_items() + index = self.get_current_item_index() + + if 0 <= index < len(items): + name, command = items[index] + + # Announce we're launching the program + self.speak(f"Launching {name}") + + # Check if command is a function (for service toggles and other functions) + if callable(command): + command() + return + + # Save current terminal state before any changes + savedTerminalState = None + try: + result = subprocess.run(['stty', '-g'], capture_output=True, text=True, check=True) + savedTerminalState = result.stdout.strip() + except Exception as e: + print(f"Warning: Could not save terminal state: {e}") + + # Cleanup before running the command + self.stop_speech() + + # Close speech client + if self.speechClient: + try: + self.speechClient.close() + except: + pass + self.speechClient = None + + # Curses cleanup - be thorough + if self.stdscr: + try: + curses.nocbreak() + self.stdscr.keypad(False) + curses.echo() + curses.endwin() + except: + pass + self.stdscr = None + + # Complete terminal reset to ensure clean state for games + try: + subprocess.run(['reset'], check=False, stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except: + pass + + # Ensure we're in a new process group to avoid signal conflicts + # Execute the command with proper terminal isolation + try: + # For applications that need dialog/curses, we need to ensure they + # become the controlling process with full terminal access + # Use os.system with proper terminal restoration as it gives + # the subprocess complete control over the terminal + + # First, save our current process group + original_pgrp = os.getpgrp() + + # Execute using os.system which gives the subprocess complete terminal control + exit_code = os.system(command) + + # Try to regain terminal control after the subprocess exits + try: + # Get the current terminal file descriptor + tty_fd = os.open('/dev/tty', os.O_RDWR) + # Try to make our process group the foreground group again + os.tcsetpgrp(tty_fd, original_pgrp) + os.close(tty_fd) + except: + # If we can't regain control, that's okay + pass + + except Exception as e: + print(f"Error launching {name}: {e}") + + # Another terminal reset after game exits to clean up any changes + try: + subprocess.run(['reset'], check=False, stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except: + pass + + # Restore the saved terminal state if we have it + if savedTerminalState: + try: + subprocess.run(['stty', savedTerminalState], check=False) + except Exception as e: + print(f"Warning: Could not restore terminal state: {e}") + + # Brief delay to let terminal stabilize + time.sleep(0.5) + + # Reinitialize speech and display + self.init_speech() + + # Reinitialize curses with better error handling + try: + self.stdscr = curses.initscr() + curses.noecho() + curses.cbreak() + self.stdscr.keypad(True) + self.stdscr.clear() + self.stdscr.refresh() + except Exception as e: + print(f"Error reinitializing display: {e}") + # Try a more aggressive recovery + try: + curses.endwin() + time.sleep(1) + self.stdscr = curses.initscr() + curses.noecho() + curses.cbreak() + self.stdscr.keypad(True) + self.stdscr.clear() + self.stdscr.refresh() + except: + print("Could not recover display. Please restart the menu.") + sys.exit(1) + + # Update service menu items and redraw + self.update_service_menu_items() + self.draw_menu() + + # Let the user know the menu is activated + self.speak("Game menu activated") + + def speak_help(self): + """Speak help information""" + helpText = """ + Navigation controls: + Up arrow: Previous menu item. + Down arrow: Next menu item. + Left arrow: Previous section. + Right arrow: Next section. + Enter: Launch selected item. + H key: Hear these instructions again. + Left bracket: Decrease speech rate. + Right bracket: Increase speech rate. + 9 key: Decrease volume. + 0 key: Increase volume. + Escape: Refresh the menu. + Q: Exit the menu. + Any key will interrupt speech. + """ + self.speak(helpText) + + 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 = "Ãvigate | Enter: Select | H: Help | [ ] Rate | 9 0 Volume | Esc: Refresh" + x = max(0, w // 2 - len(helpText) // 2) + self.stdscr.addstr(3, x, helpText) + + # Draw current section + if len(self.sectionNames) > 0: + currentSectionName = self.sectionNames[self.currentSection] + sectionText = f"== {currentSectionName} ==" + x = max(0, w // 2 - len(sectionText) // 2) + self.stdscr.addstr(5, x, sectionText, curses.A_BOLD) + + # Draw menu items for current section + items = self.get_current_items() + currentItemIndex = self.get_current_item_index() + + for i, (name, _) in enumerate(items): + y = i + 7 # Start items 2 lines below section header + if y < h - 1: # Ensure we don't draw outside the window + # Highlight the selected item + if i == currentItemIndex: + text = f" > {name} " + attr = curses.A_REVERSE + else: + text = f" {name} " + attr = curses.A_NORMAL + + x = max(0, w // 2 - len(text) // 2) + self.stdscr.addstr(y, x, text, attr) + + # Draw speech rate indicator + rateText = f"Speech Rate: {self.speechRate}" + self.stdscr.addstr(h-2, 2, rateText) + + # Draw volume indicator + volumeText = f"Volume: {self.get_current_volume()}%" + self.stdscr.addstr(h-2, w-len(volumeText)-2, volumeText) + + self.stdscr.refresh() + + def cleanup(self): + """Clean up resources before 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 + curses.nocbreak() + self.stdscr.keypad(False) + curses.echo() + curses.endwin() + + def run(self): + """Run the menu system""" + if not self.sectionNames: + print("Menu is empty. Exiting.") + return + + try: + # Initialize curses + self.stdscr = curses.initscr() + curses.noecho() # Don't echo keypresses + curses.cbreak() # React to keys instantly + self.stdscr.keypad(True) # Enable special keys + + # Update all service menu items based on current status + self.update_service_menu_items() + + # Update Bluetooth menu items based on current status + self.update_bluetooth_menu_items() + + # Initial draw + self.draw_menu() + + # Welcome message - don't interrupt this initial speech + self.speak(f"Welcome to {self.title}. Use left and right arrows to navigate sections. Up and down for items. Press H for help.") + + # Wait for initial speech to finish before announcing section + time.sleep(1) + + # Announce initial section and item without interrupting welcome speech + self.announce_current_section(interrupt=False) + time.sleep(0.5) # Wait before announcing first item + self.announce_current_item(interrupt=False) + + # 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: + # Move to previous item in current section + items = self.get_current_items() + if items: + prevIndex = self.get_current_item_index() + self.set_current_item_index((prevIndex - 1) % len(items)) + newIndex = self.get_current_item_index() + self.draw_menu() + # Play sound if selection actually changed + if newIndex != prevIndex: + self.play_sound("menu_move") + self.announce_current_item() + + elif key == curses.KEY_DOWN: + # Move to next item in current section + items = self.get_current_items() + if items: + prevIndex = self.get_current_item_index() + self.set_current_item_index((prevIndex + 1) % len(items)) + newIndex = self.get_current_item_index() + self.draw_menu() + # Play sound if selection actually changed + if newIndex != prevIndex: + self.play_sound("menu_move") + self.announce_current_item() + + elif key == curses.KEY_LEFT: + # Move to previous section + if self.sectionNames: + prevSection = self.currentSection + self.currentSection = (self.currentSection - 1) % len(self.sectionNames) + self.draw_menu() + # Play category change sound if section actually changed + if prevSection != self.currentSection: + self.play_sound("menu_category") + # Announce section and current item without interruption between them + self.announce_current_section() + time.sleep(0.5) # Brief pause between section and item announcement + self.announce_current_item(interrupt=False) # Don't interrupt the section announcement + + elif key == curses.KEY_RIGHT: + # Move to next section + if self.sectionNames: + prevSection = self.currentSection + self.currentSection = (self.currentSection + 1) % len(self.sectionNames) + self.draw_menu() + # Play category change sound if section actually changed + if prevSection != self.currentSection: + self.play_sound("menu_category") + # Announce section and current item without interruption between them + self.announce_current_section() + time.sleep(0.5) # Brief pause between section and item announcement + self.announce_current_item(interrupt=False) # Don't interrupt the section announcement + + elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key + self.play_sound("menu_select") + self.execute_current_item() + + elif key == ord('h') or key == ord('H'): # Help + self.speak_help() + + elif key == ord('['): # Decrease speech rate + self.decrease_speech_rate() + self.draw_menu() + + elif key == ord(']'): # Increase speech rate + self.increase_speech_rate() + self.draw_menu() + + elif key == ord('9'): # Decrease volume + self.decrease_volume() + + elif key == ord('0'): # Increase volume + self.increase_volume() + + elif key == 27: # Esc key - restart the application + try: + # Announce restart first while speech still works + self.speak("Restarting menu") + + # Wait for speech to complete + time.sleep(1) + + # Now clean up resources + self.cleanup() + + # Try to kill speech-dispatcher if needed + subprocess.run(["sudo", "killall", "speech-dispatcher"], check=False) + + # Restart the application + os.execv(sys.argv[0], sys.argv) + + except Exception as e: + print(f"Error during restart: {e}") + # If restart fails, we need to recover the UI + self.stdscr = curses.initscr() + curses.noecho() + curses.cbreak() + self.stdscr.keypad(True) + self.draw_menu() + except Exception as e: + # End curses in case of error + curses.endwin() + print(f"An error occurred: {e}") + finally: + # Clean up + self.cleanup() + +# Example usage +if __name__ == "__main__": + # Create the menu with sections + menu = VoicedMenu(title="Stormux Gaming Menu") + + # Add arcade section + menu.add_section("Arcade") + menu.add_item("Arcade", "Audio Disc", "GAME='Audio Disc' startx") + menu.add_item("Arcade", "BallBouncer", "GAME=BallBouncer startx") + menu.add_item("Arcade", "Battle of the Hunter", "GAME='Battle of the Hunter' startx") + menu.add_item("Arcade", "Challenge of the Horse", "GAME='Challenge of the Horse' startx") + menu.add_item("Arcade", "Clashes of the Sky", "GAME='Clashes of the Sky' startx") + menu.add_item("Arcade", "Constant Motion", "GAME='Constant Motion' startx") + menu.add_item("Arcade", "Crazy Party", "GAME='Crazy Party' startx") + menu.add_item("Arcade", "Doom", "GAME=Doom startx") + menu.add_item("Arcade", "Haunted House", "GAME='Haunted House' startx") + menu.add_item("Arcade", "Haunted Party", "GAME='Haunted Party' startx") + menu.add_item("Arcade", "kaskade", "GAME=Kaskade startx") + menu.add_item("Arcade", "Oh Shitt", "GAME='Oh Shit' startx") + menu.add_item("Arcade", "Q9 Action Game", "GAME='Q9 Action Game' startx") + menu.add_item("Arcade", "River Raiders", "GAME='River Raiders' startx") + menu.add_item("Arcade", "Scramble", "GAME=Scramble startx") + menu.add_item("Arcade", "Screaming Strike 2", "GAME='Screaming Strike 2' startx") + menu.add_item("Arcade", "Scrolling Battles", "GAME='Scrolling Battles' startx") + menu.add_item("Arcade", "Shooter", "GAME=Shooter startx") + menu.add_item("Arcade", "Side Party", "GAME='Side Party' startx") + menu.add_item("Arcade", "Skateboarder Pro", "GAME='Skateboarder Pro' startx") + menu.add_item("Arcade", "Sketchbook (Your World)", "GAME='Sketchbook (Your World)' startx") + menu.add_item("Arcade", "Super Egg Hunt", "GAME='Super Egg Hunt' startx") + menu.add_item("Arcade", "Super Liam", "GAME='Super Liam' startx") + menu.add_item("Arcade", "The Blind Swordsman", "GAME='The Blind Swordsman' startx") + menu.add_item("Arcade", "The Tornado Chicken", "GAME='The Tornado Chicken' startx") + menu.add_item("Arcade", "Toy Mania", "GAME='Toy Mania' startx") + menu.add_item("Arcade", "Villains From Beyond", "GAME='Villains From Beyond' startx") + menu.add_item("Arcade", "Wicked Quest", "GAME='Wicked Quest' startx") + menu.add_item("Arcade", "Zombowl", "GAME='Zombowl' startx") + + # Add board and card games section + menu.add_section("Board and Card Games") + menu.add_item("Board and Card Games", "RS Games", "GAME='RS Games' startx") + + # Add emulators section + menu.add_section("Emulators") + menu.add_item("Emulators", "Apple 2e", "GAME='Apple 2e' startx") + menu.add_item("Emulators", "Apple 2e with disk", "/usr/local/bin/apple_2e.py") + menu.add_item("Emulators", "Bop It", "GAME='Bop It' startx") + menu.add_item("Emulators", "Dosbox", "GAME=Dosbox startx") + menu.add_item("Emulators", "Game Console Menu", "/usr/local/bin/rom_launcher.py") + menu.add_item("Emulators", "Retro Arch", "GAME='Retro Arch' startx") + + # Add MUD section + menu.add_section("MUDs") + menu.add_item("MUDs", "Alter Aeon", "GAME='Alter Aeon' /home/stormux/.clirc") + menu.add_item("MUDs", "Empire MUD", "GAME='Empire MUD' /home/stormux/.clirc") + menu.add_item("MUDs", "End of Time", "GAME='End of Time' /home/stormux/.clirc") + menu.add_item("MUDs", "Kallisti MUD", "GAME='Kallisti MUD' /home/stormux/.clirc") + + # Add racing section + menu.add_section("Racing") + menu.add_item("Racing", "Mach1", "GAME=Mach1 startx") + menu.add_item("Racing", "Mine Racer", "GAME='Mine Racer' startx") + menu.add_item("Racing", "Top Speed 3", "GAME=\"Top Speed 3\" startx") + menu.add_item("Racing", "Wheels of Prio", "GAME=\"Wheels of Prio\" startx") + + # Add rpg section + menu.add_section("RPG") + menu.add_item("RPG", "Bokurano Daibouken", "GAME='Bokurano Daibouken' startx") + menu.add_item("RPG", "Bokurano Daibouken 2", "GAME='Bokurano Daibouken 2' startx") + menu.add_item("RPG", "Bokurano Daibouken 3", "GAME='Bokurano Daibouken 3' startx") + menu.add_item("RPG", "Fantasy Story 2", "GAME='Fantasy Story 2' startx") + menu.add_item("RPG", "Manamon 2", "GAME='Manamon 2' startx") + menu.add_item("RPG", "Shadow Line", "GAME='Shadow Line' startx") + + # Add sports section + menu.add_section("Sports") + menu.add_item("Sports", "Golf", "GAME=Golf startx") + menu.add_item("Sports", "Pong", "GAME=Pong startx") + + # Add strategy section + menu.add_section("Strategy") + menu.add_item("Strategy", "SoundRTS", "GAME=SoundRTS startx") + menu.add_item("Strategy", "Warsim", "/home/stormux/.Warsim") + + # Add text games section + menu.add_section("Text Games") + menu.add_item("Text Games", "BPG", "GAME=BPG /home/stormux/.clirc") + menu.add_item("Text Games", "Colossal Cave Adventure", "GAME=/usr/bin/adventure /home/stormux/.clirc") + menu.add_item("Text Games", "Go Fish", "GAME=/usr/bin/gofish /home/stormux/.clirc") + menu.add_item("Text Games", "RS Games", "GAME=\"RS Games\" /home/stormux/.clirc") + menu.add_item("Text Games", "Slay the Text", "GAME=\"Slay the Text\" /home/stormux/.clirc") + menu.add_item("Text Games", "Stationfall", "GAME=Stationfall /home/stormux/.clirc") + menu.add_item("Text Games", "Planetfall", "GAME=Planetfall /home/stormux/.clirc") + menu.add_item("Text Games", "The Hitchhiker's Guide to the Galaxy", "GAME=\"The Hitchhiker's Guide to the Galaxy\" /home/stormux/.clirc") + menu.add_item("Text Games", "Upheaval", "GAME=Upheaval /home/stormux/.Upheaval") + menu.add_item("Text Games", "Zork 1", "GAME='Zork 1' /home/stormux/.clirc") + menu.add_item("Text Games", "Zork 2", "GAME='Zork 2' /home/stormux/.clirc") + menu.add_item("Text Games", "Zork 3", "GAME='Zork 3' /home/stormux/.clirc") + + # Add web section + menu.add_section("Web") + menu.add_item("Web", "Aliens", "GAME=https://files.jantrid.net/aliens// startx") + menu.add_item("Web", "Echo Commander", "GAME=https://echo-commander.vercel.app/ startx") + menu.add_item("Web", "Periphery Synthetic EP", "GAME=https://shiftbacktick.itch.io/periphery-synthetic-ep startx") + menu.add_item("Web", "QuentinC Play Room", "GAME=https://qcsalon.net/ startx") + menu.add_item("Web", "soundStrider", "GAME=https://shiftbacktick.itch.io/soundstrider startx") + + # Add help and documentation section + menu.add_section("Help and Documentation") + menu.add_item("Help and Documentation", "Navigating Help Documentation", "GAME=~/Documents/navigating_help.md /home/stormux/.clirc") + menu.add_item("Help and Documentation", "Menu Controls", "GAME=~/Documents/game_menu_controls.md /home/stormux/.clirc") + menu.add_item("Help and Documentation", "Game Notes", "GAME=~/Documents/game_notes.md /home/stormux/.clirc") + menu.add_item("Help and Documentation", "Music Player", "GAME=~/Documents/music_player.md /home/stormux/.clirc") + menu.add_item("Help and Documentation", "Terminal for Advanced Users", "GAME=~/Documents/terminal.md /home/stormux/.clirc") + menu.add_item("Help and Documentation", "D L N A Server", "GAME=~/Documents/dlna.md /home/stormux/.clirc") + menu.add_item("Help and Documentation", "Changing the Voice", "GAME=~/Documents/voices.md /home/stormux/.clirc") + menu.add_item("Help and Documentation", "Change Log", "GAME=~/Documents/change_log.md /home/stormux/.clirc") + menu.add_item("Help and Documentation", "Contacting Stormux", "GAME=~/Documents/contact.md /home/stormux/.clirc") + menu.add_item("Help and Documentation", "Get help on IRC", "GAME=IRC /home/stormux/.clirc") + + # Add accessories section + menu.add_section("Accessories") + menu.add_item("Accessories", "Music Player", "/usr/local/bin/music_player.py") + menu.add_item("Accessories", "Web Browser", "GAME=Brave startx") + + # Add system section + menu.add_section("System") + + # Add installer only on x86_64 + import platform + if platform.machine() == "x86_64": + menu.add_item("System", "Install System to Hard Drive", "GAME=Calamares startx") + + menu.add_item("System", "Internet Configuration", "GAME=\"Network Configuration\" /home/stormux/.clirc") + menu.add_item("System", "Use HDMI Screen", lambda: menu.toggle_screen("screen")) + menu.add_item("System", "Disable HDMI Screen", lambda: menu.toggle_screen("headless")) + menu.add_item("System", "Set System Default Speech Rate", "/usr/local/bin/speechd_rate.py") + menu.add_item("System", "Set Default Voice", "/usr/local/bin/set-voice.py") + menu.add_item("System", "Upload Files", "/home/stormux/.local/upload_server/uploader.py") + menu.add_item("System", "Restart: Can Take Several Minutes", "sudo reboot") + menu.add_item("System", "Power Off: Wait 2 Minutes Before Disconnecting Power", "sudo poweroff") + # Service menu items will be added dynamically in run() method via update_service_menu_items() + menu.add_item("System", "Resize to fill empty space on disk", "sudo growpartfs $(df --output='source' / | tail -1)") + + # Run the menu + menu.run() diff --git a/usr/local/bin/music_player.py b/usr/local/bin/music_player.py new file mode 100755 index 0000000..8b7cf14 --- /dev/null +++ b/usr/local/bin/music_player.py @@ -0,0 +1,621 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import time +import threading +import signal +import curses +import subprocess +import speechd +import configparser +import pathlib +import re +import random + +class VoicedMusicPlayer: + def __init__(self, title="Stormux Music Player"): + self.title = title + self.menuSections = {} + self.sectionNames = [] + self.currentSection = 0 + self.currentItemIndices = {} + self.stdscr = None + self.cursesInitialized = False + self.hasItems = False + + self.navigationStack = [] + self.currentView = "main" + self.currentAlbumPath = None + self.currentAlbumName = None + + self.configDir = os.path.expanduser("~/.config/stormux") + self.configFile = os.path.join(self.configDir, "music_player.conf") + self.config = configparser.ConfigParser() + + self.speechRate = 0 + self.randomize = False + + self.musicExtensions = ['.mp3', '.flac', '.ogg', '.wav', '.opus'] + self.musicDir = os.path.expanduser("~/Music") + + self.load_settings() + + self.speechClient = None + self.init_speech() + + def init_speech(self): + try: + self.speechClient = speechd.SSIPClient("music_player") + self.speechClient.set_priority(speechd.Priority.IMPORTANT) + self.speechClient.set_punctuation(speechd.PunctuationMode.SOME) + self.speechClient.set_rate(self.speechRate) + except Exception as e: + print(f"Could not initialize speech: {e}") + + def load_settings(self): + 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) + except Exception as e: + print(f"Error loading settings: {e}") + + 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: + with open(self.configFile, 'w') as f: + self.config.write(f) + except Exception as e: + print(f"Error saving settings: {e}") + + def toggle_randomize(self): + self.randomize = not self.randomize + self.save_settings() + if self.randomize: + self.speak("Random playback on") + else: + self.speak("Sequential playback") + + def increase_speech_rate(self): + self.speechRate = min(100, self.speechRate + 10) + if self.speechClient: + try: + self.speechClient.set_rate(self.speechRate) + self.speak(f"Speech rate: {self.speechRate}") + except Exception as e: + print(f"Error adjusting speech rate: {e}") + + self.save_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}") + except Exception as e: + print(f"Error adjusting speech rate: {e}") + + self.save_settings() + + def add_section(self, sectionName): + if sectionName not in self.menuSections: + self.menuSections[sectionName] = [] + self.sectionNames.append(sectionName) + self.currentItemIndices[sectionName] = 0 + + def add_item(self, sectionName, name, command, isDirectory=False, directoryPath=None): + if sectionName not in self.menuSections: + self.add_section(sectionName) + + itemData = (name, command, isDirectory, directoryPath) + self.menuSections[sectionName].append(itemData) + self.hasItems = True + + def speak(self, text, interrupt=True): + if self.speechClient is None: + return + + try: + if interrupt: + self.stop_speech() + + self.speechClient.speak(text) + except Exception as e: + try: + self.init_speech() + if self.speechClient: + self.speechClient.speak(text) + except: + pass + + def stop_speech(self): + if self.speechClient is None: + return + + try: + self.speechClient.cancel() + except Exception as e: + self.init_speech() + + def get_current_items(self): + if not self.sectionNames: + return [] + currentSectionName = self.sectionNames[self.currentSection] + return self.menuSections[currentSectionName] + + def get_current_item_index(self): + if not self.sectionNames: + return 0 + currentSectionName = self.sectionNames[self.currentSection] + return self.currentItemIndices[currentSectionName] + + def set_current_item_index(self, index): + if not self.sectionNames: + return + currentSectionName = self.sectionNames[self.currentSection] + self.currentItemIndices[currentSectionName] = index + + def announce_current_section(self, interrupt=True): + if 0 <= self.currentSection < len(self.sectionNames): + sectionName = self.sectionNames[self.currentSection] + self.speak(sectionName, interrupt=interrupt) + + def announce_current_item(self, interrupt=True): + if len(self.sectionNames) > 0: + items = self.get_current_items() + currentIndex = self.get_current_item_index() + + if items and 0 <= currentIndex < len(items): + name = items[currentIndex][0] + isDirectory = items[currentIndex][2] + + if isDirectory: + self.speak(f"{name}, folder", interrupt=interrupt) + else: + self.speak(name, interrupt=interrupt) + else: + self.speak("No items", interrupt=interrupt) + + def get_music_files_in_dir(self, directoryPath): + musicFiles = [] + + try: + files = sorted([f for f in os.listdir(directoryPath) if os.path.isfile(os.path.join(directoryPath, f))]) + + for file in files: + filePath = os.path.join(directoryPath, file) + fileExt = os.path.splitext(file)[1].lower() + + if fileExt in self.musicExtensions: + musicFiles.append(filePath) + except Exception as e: + print(f"Error getting music files: {e}") + + return musicFiles + + def execute_current_item(self): + if len(self.sectionNames) > 0: + items = self.get_current_items() + index = self.get_current_item_index() + + if 0 <= index < len(items): + name, command, isDirectory, directoryPath = items[index] + + if isDirectory and directoryPath: + self.open_album(directoryPath, name) + return + + # Build the base mpv command + shuffle_flag = "--shuffle" if self.randomize else "" + base_cmd = f"mpv --no-video --really-quiet {shuffle_flag}".strip() + + if name == "Play All Music": + command = f'{base_cmd} "{self.musicDir}"' + + elif name == "Play All Root Music": + # For root only, we do need the glob to avoid subdirectories + command = f'{base_cmd} "{self.musicDir}"/*' + + elif name.startswith("Play All ") and not isDirectory: + # This handles both artist "Play All [Artist Name]" and album "Play All [Album Name]" + if self.currentView == "album": + # We're in album view, use the current album path + album_path = self.currentAlbumPath + if album_path: + command = f'{base_cmd} "{album_path}"' + else: + # We're in main view, this is a "Play All [Artist Name]" command + # The directoryPath should contain the actual artist directory path + if directoryPath and os.path.exists(directoryPath): + command = f'{base_cmd} "{directoryPath}"' + + elif name.startswith("Play All ") and self.currentView == "album" and not isDirectory: + # Album playback + album_path = self.currentAlbumPath + if album_path: + command = f'{base_cmd} "{album_path}"' + + if command: + self.cleanup(fullCleanup=True) + os.system(command) + os.execv(sys.executable, ['python3'] + sys.argv) + + def open_album(self, albumPath, albumName): + oldSections = self.menuSections.copy() + oldSectionNames = self.sectionNames.copy() + oldCurrentSection = self.currentSection + oldCurrentIndices = self.currentItemIndices.copy() + + self.navigationStack.append({ + 'sections': oldSections, + 'section_names': oldSectionNames, + 'current_section': oldCurrentSection, + 'current_indices': oldCurrentIndices, + 'view': self.currentView + }) + + self.menuSections = {} + self.sectionNames = [] + self.currentItemIndices = {} + self.currentSection = 0 + + self.currentView = "album" + self.currentAlbumPath = albumPath + self.currentAlbumName = albumName + + albumSection = f"{albumName}" + self.add_section(albumSection) + + musicFiles = self.get_music_files_in_dir(albumPath) + + if musicFiles: + self.add_item(albumSection, f"Play All {albumName}", "") + self.add_item(albumSection, "Back to Artist", "", isDirectory=False) + + for filePath in musicFiles: + fileName = os.path.basename(filePath) + displayName = os.path.splitext(fileName)[0].replace('_', ' ') + command = f'mpv --no-video --really-quiet "{filePath}"' + self.add_item(albumSection, displayName, command) + else: + self.add_item(albumSection, "Back to Artist", "", isDirectory=False) + + self.draw_menu() + self.announce_current_section() + time.sleep(0.5) + self.announce_current_item(interrupt=False) + + def go_back(self): + if self.navigationStack: + prevState = self.navigationStack.pop() + self.menuSections = prevState['sections'] + self.sectionNames = prevState['section_names'] + self.currentSection = prevState['current_section'] + self.currentItemIndices = prevState['current_indices'] + self.currentView = prevState['view'] + + if self.currentView != "album": + self.currentAlbumPath = None + self.currentAlbumName = None + + self.draw_menu() + self.announce_current_section() + time.sleep(0.5) + self.announce_current_item(interrupt=False) + return True + return False + + def speak_help(self): + helpText = """ + Navigation controls: + Up arrow: Previous menu item. + Down arrow: Next menu item. + Left arrow: Previous artist. + Right arrow: Next artist. + Enter: Play selected item or enter album. + 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. + Escape or Q: Exit the menu. + Any key will interrupt speech. + """ + self.speak(helpText) + + def draw_menu(self): + self.stdscr.clear() + h, w = self.stdscr.getmaxyx() + + title = f" {self.title} " + 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" + x = max(0, w // 2 - len(helpText) // 2) + self.stdscr.addstr(3, x, helpText) + + randomText = "Mode: Random" if self.randomize else "Mode: Sequential" + self.stdscr.addstr(3, w - len(randomText) - 2, randomText) + + if self.currentView == "album" and self.currentAlbumName: + contextText = f"Album: {self.currentAlbumName}" + x = max(0, w // 2 - len(contextText) // 2 - 10) + self.stdscr.addstr(5, x, contextText, curses.A_DIM) + if len(self.sectionNames) > 0: + currentSectionName = self.sectionNames[self.currentSection] + sectionText = f"== {currentSectionName} ==" + x = max(0, w // 2 - len(sectionText) // 2) + self.stdscr.addstr(5, x, sectionText, curses.A_BOLD) + + items = self.get_current_items() + currentItemIndex = self.get_current_item_index() + + if not items: + emptyMsg = "No items in this section" + x = max(0, w // 2 - len(emptyMsg) // 2) + self.stdscr.addstr(7, x, emptyMsg, curses.A_DIM) + else: + for i, (name, _, isDirectory, _) in enumerate(items): + y = i + 7 + if y < h - 1: + if i == currentItemIndex: + prefix = " > " + attr = curses.A_REVERSE + else: + prefix = " " + attr = curses.A_NORMAL + + if isDirectory: + text = f"{prefix}{name} [Album]" + else: + text = f"{prefix}{name}" + + x = max(0, w // 2 - len(text) // 2) + self.stdscr.addstr(y, x, text, attr) + + rateText = f"Speech Rate: {self.speechRate}" + self.stdscr.addstr(h-2, 2, rateText) + + self.stdscr.refresh() + + def cleanup(self, fullCleanup=False): + self.stop_speech() + + if self.speechClient: + try: + self.speechClient.close() + except: + pass + self.speechClient = None + + if fullCleanup and self.cursesInitialized: + try: + curses.nocbreak() + self.stdscr.keypad(False) + curses.echo() + curses.endwin() + except: + try: + curses.endwin() + except: + pass + + def load_music_from_directory(self, directoryPath): + directoryPath = os.path.expanduser(directoryPath) + self.musicDir = directoryPath + + if not os.path.exists(directoryPath) or not os.path.isdir(directoryPath): + print(f"Directory {directoryPath} does not exist or is not a directory") + return + + rootMusicFiles = self.get_music_files_in_dir(directoryPath) + musicDirs = [d for d in os.listdir(directoryPath) if os.path.isdir(os.path.join(directoryPath, d))] + + for artistDir in sorted(musicDirs): + artistPath = os.path.join(directoryPath, artistDir) + artistName = artistDir.replace('_', ' ') + + self.add_section(artistName) + + artistAllMusicFiles = [] + artistMusicFiles = self.get_music_files_in_dir(artistPath) + artistAllMusicFiles.extend(artistMusicFiles) + + albumDirs = [d for d in os.listdir(artistPath) if os.path.isdir(os.path.join(artistPath, d))] + + for albumDir in albumDirs: + albumPath = os.path.join(artistPath, albumDir) + albumMusicFiles = self.get_music_files_in_dir(albumPath) + artistAllMusicFiles.extend(albumMusicFiles) + + if artistAllMusicFiles: + self.add_item(artistName, f"Play All {artistName}", "", isDirectory=False, directoryPath=artistPath) + + for albumDir in sorted(albumDirs): + albumPath = os.path.join(artistPath, albumDir) + albumName = albumDir.replace('_', ' ') + + albumMusicFiles = self.get_music_files_in_dir(albumPath) + + if albumMusicFiles: + self.add_item(artistName, albumName, "", isDirectory=True, directoryPath=albumPath) + + for filePath in sorted(artistMusicFiles): + fileName = os.path.basename(filePath) + displayName = os.path.splitext(fileName)[0].replace('_', ' ') + command = f'mpv --no-video --really-quiet "{filePath}"' + self.add_item(artistName, displayName, command) + + hasAnyMusic = bool(rootMusicFiles) or bool(musicDirs) + if hasAnyMusic: + self.add_section("All Music") + + self.add_item("All Music", "Play All Music", "") + + if rootMusicFiles: + self.add_item("All Music", "Play All Root Music", "") + + for filePath in sorted(rootMusicFiles): + fileName = os.path.basename(filePath) + displayName = os.path.splitext(fileName)[0].replace('_', ' ') + command = f'mpv --no-video --really-quiet "{filePath}"' + self.add_item("All Music", displayName, command) + + if not self.sectionNames: + self.add_section("Music Library") + self.add_item("Music Library", "No music found", "") + + def run(self): + if not self.sectionNames: + message = "Menu is empty. No music folders found. Exiting." + print(message) + + self.init_speech() + if self.speechClient: + self.speak(message) + time.sleep(3) + + self.cleanup(fullCleanup=True) + sys.exit(0) + + if not self.hasItems: + message = "No music files found in any sections. Exiting." + print(message) + + self.init_speech() + if self.speechClient: + self.speak(message) + time.sleep(3) + + self.cleanup(fullCleanup=True) + sys.exit(0) + + try: + self.stdscr = curses.initscr() + self.cursesInitialized = True + curses.noecho() + curses.cbreak() + self.stdscr.keypad(True) + + self.draw_menu() + self.speak("Music Player") + time.sleep(1) + self.announce_current_section(interrupt=False) + time.sleep(0.5) + self.announce_current_item(interrupt=False) + + while True: + key = self.stdscr.getch() + self.stop_speech() + + if key == curses.KEY_UP: + items = self.get_current_items() + if items: + index = self.get_current_item_index() + self.set_current_item_index((index - 1) % len(items)) + self.draw_menu() + self.announce_current_item() + + elif key == curses.KEY_DOWN: + items = self.get_current_items() + if items: + index = self.get_current_item_index() + self.set_current_item_index((index + 1) % len(items)) + self.draw_menu() + self.announce_current_item() + + elif key == curses.KEY_LEFT: + if self.currentView == "main": + if self.sectionNames and len(self.sectionNames) > 1: + try: + self.currentSection = (self.currentSection - 1) % len(self.sectionNames) + self.draw_menu() + self.announce_current_section() + time.sleep(0.5) + self.announce_current_item(interrupt=False) + except Exception as e: + print(f"Error navigating: {e}") + + elif key == curses.KEY_RIGHT: + if self.currentView == "main": + if self.sectionNames and len(self.sectionNames) > 1: + try: + self.currentSection = (self.currentSection + 1) % len(self.sectionNames) + self.draw_menu() + self.announce_current_section() + time.sleep(0.5) + self.announce_current_item(interrupt=False) + except Exception as e: + print(f"Error navigating: {e}") + + elif key == curses.KEY_ENTER or key == 10 or key == 13: + items = self.get_current_items() + if items: + index = self.get_current_item_index() + if 0 <= index < len(items): + name, command, isDirectory, directoryPath = items[index] + + if self.currentView == "album" and name == "Back to Artist": + self.go_back() + else: + self.execute_current_item() + + elif key == curses.KEY_BACKSPACE or key == 8 or key == 127: + if self.currentView != "main": + self.go_back() + + elif key == ord('r') or key == ord('R'): + self.toggle_randomize() + self.draw_menu() + + elif key == ord('h') or key == ord('H'): + self.speak_help() + + elif key == ord('['): + self.decrease_speech_rate() + self.draw_menu() + + elif key == ord(']'): + self.increase_speech_rate() + self.draw_menu() + + elif key == 27 or key == ord('q') or key == ord('Q'): + break + + except Exception as e: + if self.cursesInitialized: + try: + curses.endwin() + except: + pass + print(f"An error occurred: {e}") + finally: + self.cleanup(fullCleanup=True) + + +if __name__ == "__main__": + player = VoicedMusicPlayer(title="Stormux Music Player") + player.load_music_from_directory("~/Music") + player.run() diff --git a/usr/local/bin/ocr.py b/usr/local/bin/ocr.py new file mode 100644 index 0000000..ea99239 --- /dev/null +++ b/usr/local/bin/ocr.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Simple OCR Screen Reader +A lightweight tool that performs OCR on the screen and speaks the results +Optimized for Arch Linux ARM on Raspberry Pi with DWM +""" + +import os +import sys +import time +import subprocess +from PIL import Image, ImageOps +import pytesseract + +def capture_screen(max_retries=3, initial_delay=0.2): + """ + Capture the screen using scrot with robust checking and retries + + Args: + max_retries: Maximum number of attempts to read the image + initial_delay: Initial delay in seconds (will increase with retries) + """ + temp_file = "/tmp/ocr_capture.png" + + try: + # Capture the screen + subprocess.run(["scrot", temp_file], check=True) + + # Wait and retry approach with validity checking + delay = initial_delay + for attempt in range(max_retries): + time.sleep(delay) + + # Check if file exists and has content + if os.path.exists(temp_file) and os.path.getsize(temp_file) > 0: + try: + # Try to verify the image is valid + with Image.open(temp_file) as test_img: + # Just accessing a property forces PIL to validate the image + test_img.size + + # If we get here, the image is valid + return Image.open(temp_file) + except (IOError, OSError) as e: + # Image exists but isn't valid yet + if attempt < max_retries - 1: + # Increase delay exponentially for next attempt + delay *= 2 + continue + else: + raise Exception(f"Image file exists but is not valid after {max_retries} attempts") + + # File doesn't exist or is empty + if attempt < max_retries - 1: + # Increase delay exponentially for next attempt + delay *= 2 + else: + raise Exception(f"Screenshot file not created properly after {max_retries} attempts") + + except Exception as e: + print(f"Error capturing screen: {e}") + raise + finally: + # Ensure file is removed even if an error occurs + if os.path.exists(temp_file): + os.remove(temp_file) + +def process_image(img, scale_factor=1.5): + """Process the image to improve OCR accuracy""" + # Scale the image to improve OCR + if scale_factor != 1: + width, height = img.size + img = img.resize((int(width * scale_factor), int(height * scale_factor)), + Image.Resampling.BICUBIC) + + # Convert to grayscale for faster processing + img = ImageOps.grayscale(img) + + # Improve contrast for better text recognition + img = ImageOps.autocontrast(img) + + return img + +def perform_ocr(img, lang='eng'): + """Perform OCR on the image""" + # Use tessaract with optimized settings + # --oem 1: Use LSTM OCR Engine + # --psm 6: Assume a single uniform block of text + text = pytesseract.image_to_string(img, lang=lang, config='--oem 1 --psm 6') + + return text + +def speak_text(text): + """Speak the text using speech-dispatcher""" + # Filter out empty lines and clean up the text + lines = [line.strip() for line in text.split('\n') if line.strip()] + cleaned_text = ' '.join(lines) + + # Use speech-dispatcher to speak the text + if cleaned_text: + subprocess.run(["spd-say", "-Cw", cleaned_text]) + else: + subprocess.run(["spd-say", "-Cw", "No text detected"]) + +def main(): + # Limit tesseract thread usage to improve performance on Pi + os.environ["OMP_THREAD_LIMIT"] = "4" + + try: + # Announce start + subprocess.run(["spd-say", "-Cw", "performing OCR"]) + + # Capture screen + img = capture_screen() + + # Process image + processed_img = process_image(img, scale_factor=1.5) + + # Perform OCR + text = perform_ocr(processed_img) + + # Speak the results + speak_text(text) + + except Exception as e: + # Let the user know something went wrong + error_msg = f"Error during OCR: {str(e)}" + print(error_msg) + try: + subprocess.run(["spd-say", "-Cw", "OCR failed"]) + except: + # If even speech fails, at least we tried + pass + +if __name__ == "__main__": + main() diff --git a/usr/local/bin/record.sh b/usr/local/bin/record.sh new file mode 100755 index 0000000..32bee03 --- /dev/null +++ b/usr/local/bin/record.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +recordingDir=~/Audio +mkdir -p "$recordingDir" + +pidFile="/tmp/game_recording.pid" + +if [[ -f "$pidFile" ]]; then + pid=$(cat "$pidFile") + if ps -p "$pid" > /dev/null 2>&1; then + kill "$pid" + spd-say -Cw "Recording stopped" + play -qV0 "|sox -np synth .07 sq 400" "|sox -np synth .5 sq 800" fade h 0 .5 .5 norm -20 reverse + else + spd-say -Cw "Recording process not found, cleaning up" + fi + rm "$pidFile" +else + spd-say -Cw "Recording starting in" + for i in {3..1}; do + spd-say -Cw "$i" + sleep 0.5 + done + play -qV0 "|sox -np synth .07 sq 400" "|sox -np synth .5 sq 800" fade h 0 .5 .5 norm -20 + ffmpeg -f pulse -i "$(pactl get-default-sink).monitor" "$recordingDir/game_$(date +%F_%H-%M-%S).ogg" & + echo "$!" > "$pidFile" +fi diff --git a/usr/local/bin/rom_launcher.py b/usr/local/bin/rom_launcher.py new file mode 100755 index 0000000..6caf87e --- /dev/null +++ b/usr/local/bin/rom_launcher.py @@ -0,0 +1,507 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Self-voiced Terminal Menu ROM launcher + +import os +import sys +import time +import threading +import signal +import curses +import subprocess +import speechd # Python bindings for Speech Dispatcher +import configparser +import pathlib +import re + +class VoicedMenu: + def __init__(self, title="Stormux Game Menu"): + self.title = title + self.menuSections = {} # Dictionary to hold sections and their items + self.sectionNames = [] # List to maintain section order + self.currentSection = 0 # Index of current section + self.currentItemIndices = {} # Current item index for each section + self.stdscr = None + 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) + + # Load settings + self.load_settings() + + # Initialize speech client + self.speechClient = None + self.init_speech() + + def init_speech(self): + """Initialize the speech client""" + try: + # Use a fixed client ID + self.speechClient = speechd.SSIPClient("rom_menu") + self.speechClient.set_priority(speechd.Priority.IMPORTANT) + self.speechClient.set_punctuation(speechd.PunctuationMode.SOME) + + # Apply speech rate from settings + self.speechClient.set_rate(self.speechRate) + except Exception as e: + print(f"Could not initialize speech: {e}") + # 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 + + 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}") + + 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}") + except Exception as e: + print(f"Error adjusting speech rate: {e}") + + # Save the new setting + self.save_settings() + + def decrease_speech_rate(self): + """Decrease speech rate""" + self.speechRate = max(-100, self.speechRate - 10) # Min is -100 + if self.speechClient: + try: + self.speechClient.set_rate(self.speechRate) + self.speak(f"Speech rate: {self.speechRate}") + except Exception as e: + print(f"Error adjusting speech rate: {e}") + + # Save the new setting + self.save_settings() + + def add_section(self, sectionName): + """Add a new section to the menu""" + if sectionName not in self.menuSections: + self.menuSections[sectionName] = [] + self.sectionNames.append(sectionName) + self.currentItemIndices[sectionName] = 0 + + def add_item(self, sectionName, name, command): + """Add a menu item to a specific section""" + # Create section if it doesn't exist + if sectionName not in self.menuSections: + self.add_section(sectionName) + + self.menuSections[sectionName].append((name, command)) + self.has_items = True # Mark that we have at least one item + + 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 as e: + # 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 as e: + # If cancel fails, try to reinitialize + self.init_speech() + + def get_current_items(self): + """Get items from the current section""" + if not self.sectionNames: + return [] + currentSectionName = self.sectionNames[self.currentSection] + return self.menuSections[currentSectionName] + + def get_current_item_index(self): + """Get the current item index in the current section""" + if not self.sectionNames: + return 0 + currentSectionName = self.sectionNames[self.currentSection] + return self.currentItemIndices[currentSectionName] + + def set_current_item_index(self, index): + """Set the current item index for the current section""" + if not self.sectionNames: + return + currentSectionName = self.sectionNames[self.currentSection] + self.currentItemIndices[currentSectionName] = index + + def announce_current_section(self, interrupt=True): + """Announce the currently selected section""" + if 0 <= self.currentSection < len(self.sectionNames): + sectionName = self.sectionNames[self.currentSection] + self.speak(sectionName, interrupt=interrupt) + + def announce_current_item(self, interrupt=True): + """Announce the currently selected menu item""" + if len(self.sectionNames) > 0: + items = self.get_current_items() + index = self.get_current_item_index() + + if 0 <= index < len(items): + name = items[index][0] + self.speak(name, interrupt=interrupt) + + def execute_current_item(self): + """Execute the currently selected menu item""" + if len(self.sectionNames) > 0: + items = self.get_current_items() + index = self.get_current_item_index() + + if 0 <= index < len(items): + name, command = items[index] + + # Clean up resources before executing the command + self.cleanup(full_cleanup=True) # This handles curses properly + + # Execute the command and exit + os.system(command) + sys.exit(0) # Now safe to exit + + def speak_help(self): + """Speak help information""" + helpText = """ + Navigation controls: + Up arrow: Previous menu item. + Down arrow: Next menu item. + Left arrow: Previous section. + Right arrow: Next section. + Enter: Launch selected item. + H key: Hear these instructions again. + Left bracket: Decrease speech rate. + Right bracket: Increase speech rate. + Escape or Q: Exit the menu. + Any key will interrupt speech. + """ + self.speak(helpText) + + 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 = "Ãavigate | Enter: Select | H: Help | [ ] Rate | Q/Esc: Quit" + x = max(0, w // 2 - len(helpText) // 2) + self.stdscr.addstr(3, x, helpText) + + # Draw current section + if len(self.sectionNames) > 0: + currentSectionName = self.sectionNames[self.currentSection] + sectionText = f"== {currentSectionName} ==" + x = max(0, w // 2 - len(sectionText) // 2) + self.stdscr.addstr(5, x, sectionText, curses.A_BOLD) + + # Draw menu items for current section + items = self.get_current_items() + currentItemIndex = self.get_current_item_index() + + if not items: + # Display a message if the section is empty + emptyMsg = "No items in this section" + x = max(0, w // 2 - len(emptyMsg) // 2) + self.stdscr.addstr(7, x, emptyMsg, curses.A_DIM) + else: + for i, (name, _) in enumerate(items): + y = i + 7 # Start items 2 lines below section header + if y < h - 1: # Ensure we don't draw outside the window + # Highlight the selected item + if i == currentItemIndex: + text = f" > {name} " + attr = curses.A_REVERSE + else: + text = f" {name} " + attr = curses.A_NORMAL + + x = max(0, w // 2 - len(text) // 2) + self.stdscr.addstr(y, x, text, attr) + + # Draw speech rate indicator + rateText = f"Speech Rate: {self.speechRate}" + self.stdscr.addstr(h-2, 2, rateText) + + self.stdscr.refresh() + + def cleanup(self, full_cleanup=False): + """Clean up resources before exiting or executing a command + + Args: + full_cleanup: If True, also close curses. Used when exiting or running a command. + """ + # 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 full_cleanup and self.curses_initialized: + 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 load_roms_from_directory(self, directoryPath): + """Load ROMs from the specified directory and add them to the menu""" + # Expand the path (in case it contains ~) + directoryPath = os.path.expanduser(directoryPath) + + # Check if directory exists + if not os.path.exists(directoryPath) or not os.path.isdir(directoryPath): + print(f"Directory {directoryPath} does not exist or is not a directory") + return + + # Get all subdirectories in the roms directory + try: + subdirs = [d for d in os.listdir(directoryPath) if os.path.isdir(os.path.join(directoryPath, d))] + + # For each subdirectory, create a section + for subdir in subdirs: + sectionPath = os.path.join(directoryPath, subdir) + + # Get all files in the subdirectory + files = [f for f in os.listdir(sectionPath) if os.path.isfile(os.path.join(sectionPath, f))] + + # If the directory has files, add it as a section + if files: + # Add the section + self.add_section(subdir) + + # Add files as menu items + for file in files: + # Get full path to the file + filePath = os.path.join(sectionPath, file) + + # Create display name - remove extension + displayName = os.path.splitext(file)[0] + + # Replace underscores with spaces for better readability + displayName = displayName.replace('_', ' ') + + # Properly escape special characters in file path + escapedPath = filePath.replace('"', '\\"') + + # Add the item to the section - use double quotes for the GAME variable + self.add_item(subdir, displayName, f'export GAME="{escapedPath}" && startx') + except Exception as e: + print(f"Error loading ROMs directory: {e}") + + def run(self): + """Run the menu system""" + # Check if menu is completely empty + if not self.sectionNames: + message = "No games found." + print(message) + + # Speak the message + self.init_speech() # Make sure speech is initialized + if self.speechClient: + self.speak(message) + # Wait for speech to finish (rough estimate) + time.sleep(3) + + # Clean up and exit properly + self.cleanup(full_cleanup=True) + sys.exit(0) + + # Check if any sections have items + if not self.has_items: + message = "No ROMs found in any sections. Exiting." + print(message) + + # Speak the message + self.init_speech() # Make sure speech is initialized + if self.speechClient: + self.speak(message) + # Wait for speech to finish (rough estimate) + time.sleep(3) + + # Clean up and exit properly + self.cleanup(full_cleanup=True) + sys.exit(0) + + try: + # Initialize curses + self.stdscr = curses.initscr() + self.curses_initialized = True # Mark curses as initialized + curses.noecho() # Don't echo keypresses + curses.cbreak() # React to keys instantly + self.stdscr.keypad(True) # Enable special keys + + # Initial draw + self.draw_menu() + + # Welcome message - don't interrupt this initial speech + self.speak("Roms menu") + + # Wait for initial speech to finish before announcing section + time.sleep(1) + + # Announce initial section and item without interrupting welcome speech + self.announce_current_section(interrupt=False) + time.sleep(0.5) # Wait before announcing first item + self.announce_current_item(interrupt=False) + + # 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: + # Move to previous item in current section + items = self.get_current_items() + if items: + index = self.get_current_item_index() + self.set_current_item_index((index - 1) % len(items)) + self.draw_menu() + self.announce_current_item() + + elif key == curses.KEY_DOWN: + # Move to next item in current section + items = self.get_current_items() + if items: + index = self.get_current_item_index() + self.set_current_item_index((index + 1) % len(items)) + self.draw_menu() + self.announce_current_item() + + elif key == curses.KEY_LEFT: + # Move to previous section + if self.sectionNames: + self.currentSection = (self.currentSection - 1) % len(self.sectionNames) + self.draw_menu() + # Announce section and current item without interruption between them + self.announce_current_section() + time.sleep(0.5) # Brief pause between section and item announcement + self.announce_current_item(interrupt=False) # Don't interrupt the section announcement + + elif key == curses.KEY_RIGHT: + # Move to next section + if self.sectionNames: + self.currentSection = (self.currentSection + 1) % len(self.sectionNames) + self.draw_menu() + # Announce section and current item without interruption between them + self.announce_current_section() + time.sleep(0.5) # Brief pause between section and item announcement + self.announce_current_item(interrupt=False) # Don't interrupt the section announcement + + elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key + items = self.get_current_items() + if items: # Only execute if there are items + self.execute_current_item() # This now handles cleanup and exit + + elif key == ord('h') or key == ord('H'): # Help + self.speak_help() + + elif key == ord('['): # Decrease speech rate + self.decrease_speech_rate() + self.draw_menu() + + elif key == ord(']'): # Increase speech rate + self.increase_speech_rate() + self.draw_menu() + + elif key == 27 or key == ord('q') or key == ord('Q'): # Esc or Q + break + + except Exception as e: + # End curses in case of error + if self.curses_initialized: + try: + curses.endwin() + except: + pass + print(f"An error occurred: {e}") + finally: + # Clean up - safe to call even if curses wasn't initialized + self.cleanup(full_cleanup=True) + + +# Example usage +if __name__ == "__main__": + # Create the menu with sections + menu = VoicedMenu(title="") + + # Load ROMs from the ~/Roms directory + menu.load_roms_from_directory("~/Roms") + + # Run the menu + menu.run() diff --git a/usr/local/bin/set-voice.py b/usr/local/bin/set-voice.py new file mode 100755 index 0000000..0651045 --- /dev/null +++ b/usr/local/bin/set-voice.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Self-voiced Terminal Menu for Speech Dispatcher Voice Selection + +import os +import sys +import time +import curses +import speechd +import subprocess +import configparser + +class VoiceSelectionMenu: + def __init__(self, title="Speech Dispatcher Voice Selection"): + self.title = title + self.voice_modules = [] # List to store available voice modules + self.current_index = 0 # Index of current selection + self.stdscr = None + self.curses_initialized = False # Flag to track if curses has been initialized + + # Initialize speech client + self.speech_client = None + self.init_speech() + + # Load available voice modules + self.load_voice_modules() + + def init_speech(self): + """Initialize the speech client""" + try: + self.speech_client = speechd.SSIPClient("voice_selection_menu") + self.speech_client.set_priority(speechd.Priority.IMPORTANT) + self.speech_client.set_punctuation(speechd.PunctuationMode.SOME) + except Exception as e: + print(f"Could not initialize speech: {e}") + # Fallback to None - the speak method will handle this + + def load_voice_modules(self): + """Load available speech-dispatcher modules""" + try: + # Execute the command to get available output modules + result = subprocess.run(['spd-say', '-O'], + capture_output=True, + text=True, + check=True) + + # Process the output to get the list of modules + lines = result.stdout.strip().split('\n') + # Skip the first line (header) + modules = [line.strip() for line in lines[1:] if line.strip()] + + # Store the modules + self.voice_modules = modules + + if not modules: + print("No speech-dispatcher modules found.") + except Exception as e: + print(f"Error loading voice modules: {e}") + self.voice_modules = [] + + def speak(self, text, interrupt=True, module=None): + """Speak the given text with option to interrupt existing speech""" + if self.speech_client is None: + return + + try: + if interrupt: + self.stop_speech() + + # If a specific module is requested, try to use it + if module: + current_module = self.speech_client.get_output_module() + self.speech_client.set_output_module(module) + self.speech_client.speak(text) + # Restore previous module after speaking + self.speech_client.set_output_module(current_module) + else: + # Use default module + self.speech_client.speak(text) + except Exception as e: + # If speech fails, try to reinitialize and try once more + try: + self.init_speech() + if self.speech_client: + self.speech_client.speak(text) + except: + # If reinitializing fails, just give up silently + pass + + def stop_speech(self): + """Stop any ongoing speech""" + if self.speech_client is None: + return + + try: + self.speech_client.cancel() + except Exception as e: + # If cancel fails, try to reinitialize + self.init_speech() + + def announce_current_item(self, interrupt=True): + """Announce the currently selected voice module""" + if self.voice_modules and 0 <= self.current_index < len(self.voice_modules): + module = self.voice_modules[self.current_index] + self.speak(f"Module {module}", interrupt=interrupt) + + def test_selected_module(self): + """Test the currently selected voice module""" + if self.voice_modules and 0 <= self.current_index < len(self.voice_modules): + module = self.voice_modules[self.current_index] + test_message = f"This is a test of the {module} speech-dispatcher module. If you can hear this message, press enter to set {module} as your default module. If enter is not pressed within 15 seconds, no changes will be made to your system." + + # Speak using the selected module - should not be interrupted + self.speak(test_message, interrupt=False, module=module) + + # Draw a message instructing the user to press Enter + h, w = self.stdscr.getmaxyx() + confirm_msg = "Press ENTER within 15 seconds to confirm selection or any other key to cancel." + x = max(0, w // 2 - len(confirm_msg) // 2) + self.stdscr.addstr(h-2, x, confirm_msg, curses.A_BOLD) + self.stdscr.refresh() + + # Wait for user confirmation with timeout + self.stdscr.timeout(15000) # 15 seconds timeout + key = self.stdscr.getch() + self.stdscr.timeout(-1) # Reset timeout + + # Check if Enter was pressed + if key == curses.KEY_ENTER or key == 10 or key == 13: + return True + else: + self.speak("Confirmation not received, no changes made to your speech-dispatcher configuration.", interrupt=False) + return False + return False + + def set_default_module(self): + """Set the selected module as the default speech-dispatcher module""" + if self.voice_modules and 0 <= self.current_index < len(self.voice_modules): + module = self.voice_modules[self.current_index] + + # Test the module first + if not self.test_selected_module(): + return + + try: + # Clean up before executing system commands + self.cleanup(full_cleanup=False) + + # Use sed to update the DefaultModule in speechd.conf + sed_cmd = f"sudo sed -i '/^\\s*#\\?\\s*DefaultModule\\s\\+/c\\DefaultModule {module}' /etc/speech-dispatcher/speechd.conf" + subprocess.run(sed_cmd, shell=True, check=True) + + # Restart speech-dispatcher + subprocess.run("sudo killall speech-dispatcher", shell=True, check=False) + + # Re-initialize speech after changes + time.sleep(1) # Give a moment for the service to restart + self.init_speech() + + # Notify the user that the change is complete - should not be interrupted + self.speak(f"The {module} module is now being used for this system.", interrupt=False) + + # Return to the menu after speech finishes + # No sleep here - the next UI repaint doesn't depend on speech finishing + self.draw_menu() + + except Exception as e: + print(f"Error setting default module: {e}") + self.speak("An error occurred while attempting to set the default module.", interrupt=False) + + def speak_help(self): + """Speak help information""" + helpText = """ + Navigation controls: + Up arrow: Previous voice module. + Down arrow: Next voice module. + Enter: Test and set the selected voice module. + H key: Hear these instructions again. + Escape or Q: Exit the menu. + Any key will interrupt speech. + """ + self.speak(helpText, interrupt=False) + + 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: Navigate | Enter: Test & Set | H: Help | Q/Esc: Quit" + x = max(0, w // 2 - len(helpText) // 2) + self.stdscr.addstr(3, x, helpText) + + # Check if we have items + if not self.voice_modules: + message = "No speech-dispatcher modules found." + x = max(0, w // 2 - len(message) // 2) + self.stdscr.addstr(5, x, message, curses.A_DIM) + else: + # Show a limited number of items, centered around the current selection + max_display = min(h - 7, len(self.voice_modules)) # Max number of items to display + + # Calculate starting index for display + half_display = max_display // 2 + if self.current_index < half_display: + start_idx = 0 + elif self.current_index >= len(self.voice_modules) - half_display: + start_idx = max(0, len(self.voice_modules) - max_display) + else: + start_idx = self.current_index - half_display + + # Draw visible menu items + for i in range(start_idx, min(start_idx + max_display, len(self.voice_modules))): + y = (i - start_idx) + 5 # Start items at line 5 + + # Highlight the selected item + module = self.voice_modules[i] + if i == self.current_index: + text = f" > {module} " + attr = curses.A_REVERSE + else: + text = f" {module} " + attr = curses.A_NORMAL + + x = max(0, w // 2 - len(text) // 2) + self.stdscr.addstr(y, x, text, attr) + + self.stdscr.refresh() + + def cleanup(self, full_cleanup=False): + """Clean up resources before exiting or executing a command + + Args: + full_cleanup: If True, also close curses. Used when exiting. + """ + # Stop any speech + self.stop_speech() + + # Close speech client + if self.speech_client: + try: + self.speech_client.close() + except: + pass + self.speech_client = None + + # Restore terminal settings if curses was initialized + if full_cleanup and self.curses_initialized: + 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""" + # Check if menu is empty or only has one item + if not self.voice_modules: + message = "No speech-dispatcher modules found. Exiting." + print(message) + + # Clean up and exit properly + self.cleanup(full_cleanup=True) + sys.exit(0) + elif len(self.voice_modules) == 1: + message = f"{self.voice_modules[0]} is the only available module and is already set for this system." + print(message) + + # Speak the message + self.init_speech() + if self.speech_client: + # Use speech_client.speak directly with wait flag to ensure it completes + self.speech_client.speak(message) + # Wait for speech to complete + self.speech_client.close() + + # Clean up and exit properly + self.cleanup(full_cleanup=True) + sys.exit(0) + + try: + # Initialize curses + self.stdscr = curses.initscr() + self.curses_initialized = True + curses.noecho() + curses.cbreak() + self.stdscr.keypad(True) + + # Initial draw + self.draw_menu() + + # Welcome message + self.speak(self.title, interrupt=False) + + # Announce first item after welcome finishes + self.announce_current_item(interrupt=False) + + # 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: + # Move to previous item + self.current_index = (self.current_index - 1) % len(self.voice_modules) + self.draw_menu() + self.announce_current_item() + + elif key == curses.KEY_DOWN: + # Move to next item + self.current_index = (self.current_index + 1) % len(self.voice_modules) + self.draw_menu() + self.announce_current_item() + + elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key + self.set_default_module() + + elif key == ord('h') or key == ord('H'): # Help + self.speak_help() + + elif key == 27 or key == ord('q') or key == ord('Q'): # Esc or Q + break + + except Exception as e: + # End curses in case of error + if self.curses_initialized: + try: + curses.endwin() + except: + pass + print(f"An error occurred: {e}") + finally: + # Clean up - safe to call even if curses wasn't initialized + self.cleanup(full_cleanup=True) + + +# Run the menu +if __name__ == "__main__": + # Create the menu + menu = VoiceSelectionMenu() + + # Run the menu + menu.run() diff --git a/usr/local/bin/speechd_rate.py b/usr/local/bin/speechd_rate.py new file mode 100755 index 0000000..ddf32f0 --- /dev/null +++ b/usr/local/bin/speechd_rate.py @@ -0,0 +1,303 @@ +#!/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 Rate Configuration"): + self.title = title + self.currentRate = 0 # Default rate + self.stdscr = None + self.cursesInitialized = False # Flag to track if curses has been initialized + self.configFile = "/etc/speech-dispatcher/speechd.conf" + + # Load current rate from config FIRST + self.load_current_rate() + + # Initialize speech client AFTER loading the rate + self.speechClient = None + self.init_speech() + + def init_speech(self): + """Initialize the speech client""" + try: + self.speechClient = speechd.SSIPClient("speech_rate_menu") + self.speechClient.set_priority(speechd.Priority.IMPORTANT) + self.speechClient.set_punctuation(speechd.PunctuationMode.SOME) + + # Apply the loaded rate to the speech client + self.speechClient.set_rate(self.currentRate) + except Exception as e: + # Fallback to None - the speak method will handle this + pass + + def load_current_rate(self): + """Load the current default rate from speechd.conf""" + try: + with open(self.configFile, 'r') as f: + content = f.read() + + # First check for uncommented DefaultRate with flexible whitespace + activeMatch = re.search(r'^\s*DefaultRate\s+(-?\d+)', content, re.MULTILINE) + if activeMatch: + self.currentRate = int(activeMatch.group(1)) + else: + # If DefaultRate is commented out, get the value from commented line + commentedMatch = re.search(r'^\s*#\s*DefaultRate\s+(-?\d+)', content, re.MULTILINE) + if commentedMatch: + self.currentRate = int(commentedMatch.group(1)) + + except Exception: + # If loading fails, we'll use default value 0 + pass + + def save_rate_to_config(self): + """Save the current rate 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() + + # Check if DefaultRate is already uncommented + if re.search(r'^\s*DefaultRate\s+', content, re.MULTILINE): + # Replace the existing DefaultRate line, preserving leading whitespace + newContent = re.sub( + r'^(\s*)DefaultRate\s+(-?\d+)', + r'\1DefaultRate ' + str(self.currentRate), + content, + flags=re.MULTILINE + ) + else: + # Uncomment and update the DefaultRate line, preserving leading whitespace + newContent = re.sub( + r'^(\s*)#\s*DefaultRate\s+(-?\d+)', + r'\1DefaultRate ' + str(self.currentRate), + content, + 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 adjust_rate(self, amount): + """Adjust the speech rate by the given amount""" + # Rate should be between -50 and 100 + newRate = max(-50, min(100, self.currentRate + amount)) + + if newRate != self.currentRate: + self.currentRate = newRate + if self.speechClient: + try: + self.speechClient.set_rate(self.currentRate) + self.speak(f"Speech rate {self.currentRate}") + except Exception: + pass + + def 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 Rate | Enter: Save | Q/Esc: Quit" + x = max(0, w // 2 - len(helpText) // 2) + self.stdscr.addstr(3, x, helpText) + + # Draw current rate + rateText = f"Current Rate: {self.currentRate}" + x = max(0, w // 2 - len(rateText) // 2) + self.stdscr.addstr(5, x, rateText, curses.A_REVERSE) + + # Draw rate visualization + barWidth = 50 # Width of the visualization bar + barX = max(0, w // 2 - barWidth // 2) + + # Map rate (-50 to 100) to bar position (0 to barWidth) + rateRange = 150 # Total range (from -50 to 100) + normalizedRate = self.currentRate + 50 # Shift to 0-150 range + position = int((normalizedRate / rateRange) * barWidth) + + # Draw the bar + barY = 7 + 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, "-50") + self.stdscr.addstr(barY + 3, barX + barWidth - 3, "100") + + # Note about saving + note = "Press Enter to save the rate 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 + self.speak(f"The current rate for the default voice is {self.currentRate}.") + + # 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 rate by 10 + self.adjust_rate(10) + self.draw_menu() + + elif key == curses.KEY_DOWN: + # Decrease rate by 10 + self.adjust_rate(-10) + self.draw_menu() + + elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key + # Save the rate + self.speak("Saving speech rate to system configuration.") + success = self.save_rate_to_config() + if success: + self.speak(f"Speech rate {self.currentRate} has been saved successfully.") + else: + self.speak("Failed to save speech rate. You may need root privileges.") + + # Wait briefly to allow speech to complete before exiting + time.sleep(3) + break # Exit the loop after saving + + elif key == 27 or key == ord('q') or key == ord('Q'): # Esc or Q + self.speak("Exiting speech rate configuration.") + 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() diff --git a/usr/local/bin/sync_time.sh b/usr/local/bin/sync_time.sh new file mode 100755 index 0000000..aea1925 --- /dev/null +++ b/usr/local/bin/sync_time.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +date_time=$(curl -s http://worldtimeapi.org/api/ip | grep -oP '(?<="datetime":")[^"]*') +date -s "$date_time" diff --git a/usr/share/sounds/stormux/.gitattributes b/usr/share/sounds/stormux/.gitattributes new file mode 100644 index 0000000..d899f65 --- /dev/null +++ b/usr/share/sounds/stormux/.gitattributes @@ -0,0 +1 @@ +*.wav filter=lfs diff=lfs merge=lfs -text diff --git a/usr/share/sounds/stormux/menu_category.wav b/usr/share/sounds/stormux/menu_category.wav new file mode 100644 index 0000000..1720a28 --- /dev/null +++ b/usr/share/sounds/stormux/menu_category.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa3c7bfc7fb89a8cf457f614800b1c6864e75e73498f41741e9ae9980f98c185 +size 79424 diff --git a/usr/share/sounds/stormux/menu_move.wav b/usr/share/sounds/stormux/menu_move.wav new file mode 100644 index 0000000..9c0773f --- /dev/null +++ b/usr/share/sounds/stormux/menu_move.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c313b4c70e7b6fb4ee4b9eb21cd90ef3ec335a3be6565cded42d42e1e1c0821a +size 48044 diff --git a/usr/share/sounds/stormux/menu_select.wav b/usr/share/sounds/stormux/menu_select.wav new file mode 100644 index 0000000..3803207 --- /dev/null +++ b/usr/share/sounds/stormux/menu_select.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e5ce46d4c556edc642c315c8ba029dbf7c67a9263379d312324263f790e307c +size 144044 diff --git a/usr/share/sounds/stormux/start.opus b/usr/share/sounds/stormux/start.opus new file mode 100644 index 0000000000000000000000000000000000000000..ef542c9079b4b11d6cb354dfe993ca9d27ab7396 GIT binary patch literal 49878 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

EsjX+W1(zD&KS!Y7lFqB4FXAe% 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 YbDH?td!onf?(7!Y*jyla`*UL(0OM?A(f|Me literal 0 HcmV?d00001 diff --git a/usr/share/sounds/stormux/stop.opus b/usr/share/sounds/stormux/stop.opus new file mode 100644 index 0000000000000000000000000000000000000000..b346979868f2809a2cf6bea35c6b420c3427361c GIT binary patch literal 49309 zcmeZIPY-5bVt|5|6Iu*G%=m-XO&Nv#3rdSUQWH}cnJgF^c7wzjz{z{!9Ot|1a}j;lIp( ziT^VH761JI2SWGH+OIeleB-(S`;-f3Vwf-Jztrwpd+xR1)lwg)m$SA@tC;M6ev5G; zy9JkFT)&;>@&$KKmnkX7c7HWeUU6$pw`8Nq?gO_91NTJfCA^kd*fH0W@6dX_vjVpH zEys2rzRV`z6BZ@vc`=Q*I_KK{7yr1ATUv%oiZyC9KGajQp7Z;0iq0ujFH?Jo#l_40 z%$}Q9?|tz5O!%C?F6XK)#(h?O$KDcOa&Mx@%97)2-wI!8y?9+j?6+cC;iZQmZg#;^Jm+!;*{ju zZNd$ji}??Em{zTm=f10w6mnp4kb|Cs=5F~3^#XV8ycg~%UUYc2f_8-G;im=HPn zY;ybK{2Yl3TJC?YRz%*h4BvgK^Ga=9O=A7Szl;pw<%U~N*c9)db=&5&Z>{f4$-fyZ zE!4g@ZMn7ob<4WfZmxlMvjgAmo`1&T$oAUOn%?<}8E4)ublv^u@6`*>D|`ROuv~Cq z_iF!E`pRVj-?qElFP2R*tSn8qedl=o!zzO{?&l1C{0V(Jwez9%%Z)AW6BeAh=_KIF zu%@}AYT2HXZ7U94`Sgx|(qYDV(!UHm)S8Z7YkMb{84zMGx#r%Nfqqi|SAK=B3p2Z7Jhm(9P(RJ%-DZnN(U z&%=A77fkh*ImEv0{k$pS*riAEGwlD1OaC&wH2-Y-=fCb3da7q|&ekz9I2N(e^ttuy z6~DcfCcNAqRGxEZ>V-A~Y1wyI%IYgJTsmGyc(VwKr&dU|3T|V5bt&+zu;>R{hj&NS zp9dY-63fwISnYIs(RI78Q`3$|>uA3DY`0C>sevnV#rr3Z>^;&sqF-}<@8g>1S1jtk?d@rA#;~&)Yb#^*LZ6hSi~l@k>(jhbqar~fqhD?2+}4gm^$DvuYwovd7|Fc9v3~a6JgbJ# zva|Yc=k;FQz?GUW@8s7fJLT@ICvRVDapl-u@SB;QH~r5Z+auF90W z6d9FU@YZ>anxrkOZ|sHKiStz!Wxbi$Ayux$%k(Yq$DfCPZiYNEG%@O0*s4$)>%04a zrJvn|WfDKyWt=>C8a3D7SaZ_-Zl|M%MNH{&#?=DqzYau%By3n-Ds||;_w5T;=NoQv zuJEq-Kl#RFxnd9dBOi|~vhw=Mz!|<$FW;jvw2VdAi>GYs48ci4ng^upTRd0k#rR-RN#o-UfV^yR9zxy$FdUA9p&H<ZS18p=5t^Xovf<((P+Z+%79l6)0g# z4zBeGcUaVUf04HP?JsNE=4!L+-mW|-r4@EQajL?jJNid<)K>l9UKDyk*TsXK|4`|% z!e04TOIGYWFT`mlxY_G1-`7{p(;1eoT`QgR&iT~T`oC^^Yj3`MXK{YUHw}|3I!hk@ zxoH&byhcK<;l<_QHm1pOn__6jrN` znEmuj#ItFWL>``ZjVm)>kixxf=2NX}Klf)QO!Lp#k@()a;qRtAi+A&9YNhRuw3%1f zx#IeU4)3QjB3(are@<7oxw*OY>y8Kfe^|nIWY|m#fAa8;fs$c=bm`$6PyZi&erlU_ z$MP$}8c#NPz1`FEDTC9yXu`xL8uqVWH1Rx&_FB&8I;Cj=+XqS82X~9F$^Uql>d`%G zdq~l;eD+7W2P11rB^n-ncepv{zt4d+Mm)1x>yuSO1kcLuSE||XbxUGpr2L|(I~x=f zTNdTSEew|Z!@*Kx$FV20E#PMK<}0$kKY~xLd6=qxFuTe4&+p9v_ANUcI^Vxod2aLN zihv34I3D;oy19f*v0c@6PV%Oun}s$*s@O&mqeCewA0|DymGOK|<`MSh6P>b$BrM;B zvj*p;?C({GC}!)sa7nSr?D)Gs?P={cmh$E^FJ&6tHD_NcX}yd)pV^SLC|sep2mwlma{Fw8BlUMVlq_ zO>6V^&vD^YU*F!FlVlO9JpGvJ%tPwIM`o;V%YR^(^6Pc?wpl!rt{6>D-=bi5DfY0n zb$(j|qfhhU9;=uZ&D|`FEdOSHkUwzv@+2<*Wp`Hh%|GWOajZ=${zxuIMeV``xeKQ( z;y&E|gVoqvGtA?UesJZyMZOCxbL@|_^X2_M=DO8nZIQHUb^-ID*p6qh-ySo(_!DR^ zb9N<<|Mw(m-GH!$8FHb&vx_)=ZNF^ekr1sbICE>^=X$?q^4`T3e*gOBomD%(XvbDD zHwV`8+e#r$`bx7`AG|L*VPWr!lWD7!o8DXM>^ao#y~gZo=lm5vcb#m@Qj)(sL+S3k z2hR$s?Jmd7(&TRD4^7?aU^uI|!Kqe{!9MDIk%{Ie&h5<~@>gxz;@8>nx_I|Gxoum{ z-DWvmBNM?QY^yd+#oo{4)AopZlepPR3T8h{gKlmAv~6~<-FL6OMx{Y>3U8@Y6m5O| zN3WGpg2P#Ijbcg~%Z%dIW1IgOKXT>ObWwSHq)+0e#UU2AG|nD#)m>lZj@p?>r6_`3`x9kSaHn~`_k*pbMwN)o@YL-+!)Yc?k<0$Y2I_GGk$q4X@#4kPyIdi z=(y>Qob^?PKYfKH=DmAkd3{TMlV{PyxyCd1t9{%R@KPgq*`%Av-;TEJJR>xxwJz*a1i#YB z-sNe_gu<_8?Wq0w)hSnMlJ+LA)4PBD37Oz7^5FY?wvP`tE)P8__hPyxbMNgWzL)i_ zYl6=3NgCXD*{qoS#`&vamlO%Z9Jpa7?miy$vws~)s zgn1pfdm+ep_rc|Q>H^jl51otiQzW;Zd$^gg_4xRFLS5NE;klg)!5nXuHwDu-3bY(;&EYLN7I9K_ob6p_5a+q^M&VSAI^f*VzcsZz9CB% zxnA^ITQ;5Z`fJx)LbuEc8F%I=%YK>dez;DmXT{28%cRdfPtuf%Ej#hO^i*wR$IKSy z&PSDjW}i2%i(0O$DI9q6OquD5)N2;IIF3H!og%!#adp4x!iI;xCLFxs{CwV*mBJ^p z(^o%douu*f;1kQQkFS6HaaT}yCrjzOO)d{Ic^-ts##vFx9-0?kRAA3gS~^*1E+gwTsU-WPer|Nhxknm_+&)4xTTng!C|Q*!wEd&Vr_5&&fB5qp=O2q?4E`Ko zx~Da^t@Zszv$I}j-c8vdR3vfET19XL^Cqbst8;k{H-0bDxM|jPr2EC33vEmf7}$36 z+pCL&Nc1ZkOH}H$J-$%>h=0CwrN_eK7w3w`7BTCK>>NjW*_#tq?t3!D_bEm zGx|#N1nC|A1z)b*(9x<4(UH8p+--NEzSm*<>}io|YkVqu|GT~OV)DstIy7Z*z`EUf z-}^E*&fU!_wWdM++Jr>2&>hzMSFK%Zvz9}Pt){B(sQ-#EqvZRHhe}+&3LWO?+o-4ax{&Qg8K%vIT2u0}G+zr54rY1i#KQ&Qn})wcD`346bB z?GfVlY+DrZQfiU+l$UMmeDyOuKmE9VCsMB1;p~GqD?Z;`x-lTcZ}<60THn7)MDViM zt==~~I5+FuWTwPyhT01*`-4^g+?eV4gm01L#aXJixo(*32}_#x-&$net$q8mKgZ4p z3Hmwn_9y1Jg^bpU%UA{Ke&49?o5p!?jZea}^xfQ(n=h3d*FHL1u0HYQir5FYl1_BDai!b#bV-1>oBR6E};;Tn8;>^31JvsH}Z!v3q z|M5tWS@`2MeA&W{``lFK+?4-vh0)O4#rA5Q|D5kjcy=w>ljD_jpwWL(2?~XopV`G;w8URHu`copY66XXdjDeq9Ae=ls3= zKyLd^&cYV)c*g2WrluZY&+8^$+pISbz&3asW1Jaghz>zyRQ ze!ZSrhry}xC4ulX&`C#xD}NIPUcRWCc`)N%Rnu1V{X_sCswymzYo{Yu`~LQ)%K z!&QBht6XPX4?S{>&1l<{=_hjjH`(>SNtD{*$)K_RpX~YB@p6nJ&yJdR>^687JyGRR zyz<=hX_py)2mj+-J@JOpk54Zwn;+-dCAe^>WT}0WWb;^{$}F{g?p@=w2)^VZP5ZAx z`|IwwSnT&OTCMu$>V00R?7u6*4DK@gHP)IG`)JjVhZZ_34O%B}nRd!f>^y_y%y zW=CB9D_d;S`zy5<4K^upp5S8CF3v94(sbmdlg0jk-X$j-Rqx2g+)?qKeO3CM z=Dy`BEh^6T6ZiXGv)BC@`1-9<#J%TgUrTl8N9t_&xqXWt*Su|SBxl%bo(@#>?v_sQ z{r1qS=IAm*UxUV*XLxo`=Du#0cRhb=|EdSo-|lI@`_b2AJpIR~MY|uT{P-C7Vt(tT zM#-f+UCx_45z%1Xdz!)7W}@rv;?Tce4Rcs;JZ{*qK=a0i%FZ`2i#-)Dde$a-hOZU!_qXt zEe8ax1rDCkuQ51&#KXDUn5*JTJI}<91IEd*XntmOL?Ir;Z`LH;kTx8x?f7V+|QNJXUz+6FE56#Z9rg|XeE>Y%-) zqtx5|c6l?kkN2-ywu&?J%gv;<4qNZ11ix5xPLF4rjn=O@jx7hCDw{Z;WNyuz9HMwy zW#X6>3eB}z9k@chj~ zH{uOV-^n;%_S|^|lxq&QJyc}ZOR7Il`z=>hY3@i{7gzqZ6n{EyQUt&ohYw^tW8t@}L5e#2RomT&vZ zAK&Y(={VH)`goA3&E#*TN22DjUy=V+8@Hic>Rgy*Zh-KFcNZJ?9F;i8`A3{LYM;~M zDSPK13u}*k{`0M2F6Xai-n9%#b3e5?ho0z~b$R!?)i3f2$4(6ODMavY=N<;ak@| zDBV@$Klw@KsT0d=nUa^YXwFz;*}tZ|vtDm=xyz!qb3tZr_K8gW>%v~Nu+MSc9sffm z@k&k)%3c4tz2@Z+XRDpJ{T++y66NsD15cyOuC#Yum>BhC>Q^F~uwMY7?{CgTv>d4*xkk zeT8|`r-|H(Yb3i9mb{SJe)?d2!1Nh!vr{B9oOvWD1+qHw^RBw>yy>1@bmNiC zv<9Q#9((UycXIq}4xHNR%4SXLVLTj>_hthhguF3uA?JB#6(vAoZzCkJIZ zIg8Xhugov0c&%LR>(08J<6F)22&S!0zLu6h^6NKuPr0MK%6!e*aGQ_I_OTw==ReD) zC3f=i=-+?8zjm|xn{!J-xL#6iP9OL3UB6}T8NRojS-s=dtf1}B^!#)xihI_ z=NWdp#(&%XENRl(Ti^Nlw&~kjZrVBLdbx5yq0&-gBqsIi`o?WCH(&ZY8t3l%QNpKv zb`S5{g$h~4_LI3^JH*@XeY$7fAF+8CSFCni7h^g#>E3*+x=VgrKlPuudr)toR%KuH zgoVqL6t9WJJTAze+Lfovd~)vDweH_SLell+o0U_YYvv#KHFr@a~f7o%XWbWk*L7tJS-m5~-Jhw)_3fS?6;5#Flj%KCPc}smIw;)j3sF(Lu>J zUNHK}+RZnXvR_D@E~fJMTfQ7Wi`&X$xix`2GF}I#75{&gV(f4G?N7qpXT>)+;?M&;RYv1fM)y{ahOtm;08^^joy?9b8W!uKbeRf;q?|50D|TE%*a zc!=tst0|IJ`$LO8p2b()=5LfROnQApsbO0|P^gCU=5oE=Pv+M~CQkO&yU)=6qx{77 z#3eta_Zcc@+)ygzjAz<%)VBP;M`TsZN>^@~iT^u997S_Yeu=V2pKe+-VIRAYb8D=) z?bqGM49^F)THUt#St@zG@BIrMNgdgB%l=pHer$T`RrE*AmwjJFPtTGR-BP8RuXD1c zy65df8y?a7o8y~OPbePX?#cRb=0ri-vaL%G{(N5acl)9Jf1lU7tlGMJLk8cyU2YOr z1bExtYp#DP-5$+)L9n`tT{w14;y#s_n39Z5vQg8QSKqkhF;6v*_ulTuYbNd4JTu{t zO1tWdmdGskOJ_F)DltsIyi%06W!<91lg}<--2&?IO)8N4{WLE*c&%yOo5R>i^9euk9KNuYY>5(Q2{4nX6ADZ+$cRsptH(IP(en?b`UB$?Ml=y}xm_ zLQ|PdiE)YFt##jQ7w!<^SBSs6gLS%+wankc#@q^}o>#JqKF3_w3AfjEciIwqtgH9K zCHWF@|H_rCjXHj5a)o?amhAgE%P4HciB%@X z#qvjXKk8BabJJMhtpBC#xMsHQOW}t&tmHp9uKqN0ug>$SA+uOHl|TR7x9n=x@4xxU zPrh@P%?|c`yL#!*~nq z?f%%gSD!N1vKFS#5BPb z>|D)m>0JKVyK!$vUa@QHERhxOkk8uwr#rI6a}Q41&}*X-_>*gi>Yp1sv@b3b-RE`j z^-htQwmVL$FRE8gRf#;KwK$R6O=U~RItPJyTYcY(_@u2fU7`EK%IukApzD5AI`6UJk`%|CUZ;isoiDF;s#(Psxkf(v@MJw1dzP@*dYM3wI?nznQMIL+WDY zTk91u58cBfZxxmW&FH*c_WSK_A?@4FMr(6V#(H1%aGAHEGi>6k-M-T%X58&OerDp1 zou?-pD!4ykdz;yh3IE^8-#)cCzXjZgy=cEG;5^f`{tK1`5neiRrgJ6lPVDhZ_5B?G z#(w4D`?2L#pFdi&yp`OSF~7z=+bKJ)fro2>Kqg;Njql9`s{>C@NV(qOCu+Fa%SKe# z^QGWl&fi}cq7@IW6Yoyn%~ZRr^0M_+XHmyC$&~9Ag)3Xa?e0_v6g}%U`c=86MSHfW z;r-}`*Pmo*tnj$f@p<$AXBP!uKc)jGI+8yhC+wo%F2AB5V-CJU$rOvmm&t7XLTX$2T z|A^|Jo0`*-s{T$%oOm$A`AF-}+LQ+iQy64qzu2}F!C+; zc20Glt$gBVOvLN$i(cH{HMuhU6Wigeh!n@_F9&59IL>(`*=vS;W_t2bV%sl)-y5n{ zwJ5D!`F``A4&G%Y4>qP1&2n9|AY#V$=B(O{y?F{8&I`|SJwJEP_4ct-Z#07UFi+cN zQ+%!IYxGsF4e5UCHYnVEyjN+1S7hf7cR6eBgH!)Gu3EBlUKXGCzP;i)3O&|;yEaOf zSbhn5tpE7Js&_LkoNshHai#y~sSWOi%v%|fy(1Kv_vl}^;hcT@BL7}%j*HQ6PEBol zv+K3NnyUNno}MXt8vOo5MCV)88Am?<-1-0S>DV7Zm!d!|*{JtQwRSw5YUPB7m;yIhA$q2^_A!bxM3NeX|ac0IlOc-H60dFrRbwm#sgWjS_| zFMr+*&cF@Xb2l#JQ>&Z6|2SAos^yrafoh2CN?Cr>b-x*|Jyj30v03c9X5)XC7Yo&z zH!GK!3vU!#WZyUY#qM1frGD*V+7W*2_??$;ue_agQLwC1_wdr2+82_lSG`NuKlVaK zNwY`3aLcSPv4sDDd6jFFza{Y3@x)4G{Cx3j*G0~cw>JkjEZEa_`}8HJ3o=Z(?|)p7 z*pt;Z`Hn>c|8e0hA%+Us7eq?E%Da{2+S68SOrD@36M22*r(JWWz7$Ew6S;85;Z|L7 zQGCf|kMykPf^zy3oTI+lxTyZQ8oXVxbK%;}8qCc)8i?OmNNDiyrfAo*2B`(Z6f@ z5uPW@H-ybaYEh{^iLg>Ty!w(QYd6HnJh+)XReIl++ydDS>pyR@^Cd?4g8)k1B~ z)0!DvZiZZqEl0O*v8#-6|NETh_t7ZG;;&`U9&S#LB~p43d@)bPny z?sZMm34CtySMz#Z&*lc*GKB+|)$6;PnznrHsy&f;lYvcoQ|V5p2Vd@djjv)jd3;mr zb=BniSI*=;6b)C2{-toKb1svXTIOZh{G#(~V&<7tWv@B%KGfUHoUKJZyoQm7Y5LrM z|Ms7M!s;&F)A;FIy~dHpu{ZZu?)}DdBx|{ozoikY(Dm5@Kh*e*C+yYIr_yIDqS}+({n*y86i{(s5>fqg(~hg^`J|xT(YE}FQW3`s zxaOX-d{w8YB~Vxzerx4H?x}|V7Z=!c7TxTcW%yyDSpBoFlS@`#-Fg4|M4l<%?YA5A z{gu6+k>FQ$eszZ2k<)MPoKt15zM_A$u=n7#m*;<k?~6>>`1ODPI_6ym?l7xz{B+MdzUobD%;E`m zWOF|^9De5de%BU3&YHfz8v-x7O*`~IPjS!FuiZ~q%*g6YV)Hb1H*!?T*RfRmq4rpr zHIAEOQ+4?InO6*0AJ6pm=QCa0P%BzzcDpyYLTB4ErTqpw~Csz z$K%GGBiV}&FFfG&LvpP^k%-Elt0C#%J|3A?BVEh9d3DpXL(e7!oDq5My!DP-obYAN zpyP5j|DNu>$=l#;;CtRpnfs;g`d4Px4<_A`n!e)6>r+}M?Q(y=`*Cp3!^JM{ZmW!U z}oHKiJa+>5`$D`lY-nrQ!YjW>|_^j5(r5TTf z`?E`59i4Gv!lD9`+`UJQw%?Nr>CE-kjQ_P;XkK;BwK`kdU%75M^X0Q=YG{nNz{Tl5wl%Ig$04wjr?~C$_N;01 zRgb26)hiyz)D%wpn)&lQ(;c@xf7HTwS@c+L>_}kE&1m;nchr92%%0-b0;iO(J0Bcd z@j=&yJ%V#%;8o)_Du1q)HQ4Z;JhAS5XRY9~bLyc}cXn3)=t=u3rup+I+a~>)w_aMl zIp?!h$n(qL?TY*Mw?F!lzenaqP@h0W-G9d!$J}k!b0u+FT-qVArX$=_HXz7yV^xXE?!wdPUhSIs<(Gu;>P1)g4W_Hvom-X@7Vx${+~sU17C>CmHD)3+{} z@i=B_NK9jxX0;i^u^T^S%(RyooiHqqJJrK4cJ{n=>a$0uCK~*35O=nnBfC2ALFpfr zKUW{J?A!dDlZz|s!wL3e-I(ssDa+TC2*GA_ZK z@ywTBD%Jk(b1L8#E*Hzv-&VizT)2tJY^%takQtP?!PBGN3}gE#og-6=bU#}UkWU7R;*%_br(L%_v4_NxRq|&wB6oZ1%|o#kHUXW z@VUXqxoTFt=2xkDEsIaLOaxc@~ct-RZqshqL<$M9Al92 znDZ!BIa&M;4Pny;D< z1AkhvhKb>Emv_&nOf7nNe{NgDeKDCe{P!KJ_udopS-YBJO1!nVcNI_H6@@*Qw1vxm zpMAXe@xw>{%&YW5go~^Mi_FEVCoQ#7{d2W`J%dq1Lu;JD^{54m9dA-T%rrcDZPy+D zW|{D!?S%&QNjoyG-h4Ih;ryAJ`=9HY%vyPE%T2Kr2iDXunnle@wL6%+ZtuD)T_$`E zADI5Gbf0N`Ta#ziVZqp+*?j+)uQ;)90$xew?!<`&g~ok?+3dX$v*qGXAewkSlim zuIb$e8Dm|a;8tJe{P0!cYWM3M?pY#+mRP*Ra7KbpDCI7>_zjOg5VR9Q$IgH z5-gKBZGnrL_xd1K%`@8jAM9oOrZ;P<=8H3t3%6$RNzDyD`A~O;WrTZ3)yd;q7OdSU zv9D(K*6?u_piRGo@|+?kUq$OJJ2L;jeZam+JV=KdCVZRo(O@wzg*`IZxlnb@~kF?Unv{r&rQ z$JI4LpxWxv?)R;>ZsOgMh5VaL(*7|@RPg*xTN5xvYMn{w%(u&KeZIF$?aYN~b2qje zRGYTvr@Gy7CC!e1U7aT;{j)mwa_{V;Iq&YRe!Q;Cij|3R0jp}t&uxY=-_?DxrcT|Y zwQJS%V+MC`T>l`aoif8Ce^X-HC9OTHzIMDgv@7%S_M=U67*4WgpNp_<-oF25R({j+ zqf&RS{g8dLuh4MzzT&Lx@~(p3RObV;YmYv8Z=t$s?W4SkroYSjS-z=Cd3(2PWK&r- zd;8Bdt>w;{k3O33@SG6*H*9UvQM-dDi>nS_HT?ZfuV_|Xz>@oKzWu3Ixx3gRCc22N z;E+eB_&>)YP1+J9M}<0R*=fWy+}R{LM6{JEOAK|}9T*B|#z z?z<`XUAA#sYo|8-nQ$XhJpcH_;Kh%wDjf6Pw#>wleX(4Y+?B;!jRev)AG&Ny&GpF0 z{KK|~-#Bd6vx_wxITQK&Zlx|S4KjNdw@pW`{asS`lPh1BU3FZr<@~!#!D1^;#^uUf z-XiE@#&vlAvzlwIA@jUuir>tA%K3u*v(n0~J;ydY_HkbN<8qFec*D7h%=O{z+vbTZ zwF(nFeVX^n{iMv?b7y1U`AmuRbrCbTVr8JRO~O&TW7CzQ=zT?ojG7Oc7nU9NS{fyE z(&K2kppMl2=+4`37gd~`A0;1V#k$<-?3>n8kGU5JZ=2W2c&TbJ<9^+>VhnCVpOx-) z9GkS|=8|HbgHH>u#2-^K>o!S&~7i{!nU($9L0X+m5W;x`|`q zwHYfWpLyI+zxdwP!yL;6UA%fGcL%PUZGG0BLqw?$V?sC@)XZ+{gGHIAPkJ0h|uX(~+%fB6KzYrb&_~Pp~SsDg`^A~9| z2ix2ia4?s46Uo@E9uR9fAecSgI}h79&6jCO}zCB=J%t@M@7pSP$> zpOAK8dvj#(gg9#jV}pghzVGUfycTu*())h??Oj!|{p|kWC}k__ z;8~XI&c8jk?va~S^1mMc6kZ+|6@So{GX{oo(9SUi@J<@|FZvO|BL;X_}}qg0lFu~?7#GXng4SCW&VS9#b|-mivO4WFY#aGzc5%% z;=jUw74Uu;h>0Ncl>W>8SNn6L!tdJsIdL~vd~!Op`j^fw%W13k%a#|PP3PQZ;NrU@ z_27kFZ8M`iFE3^M_gG|u#TSWBf9(9;Km1xQY-eC@;O;#2)w4yX|LaRVJ$JTbA7}B7 zo%(HBft=q88S^gl-hO+`Y=+0#zkJ)-+Rqe3$ZcI(sa9|3|KOQa@1z2)m03sc+*etn zJ$qJ|`4ZunZI`!8*{eL>Rq`)e-NsT)s5;i_6vf)79T$Kui1j}og(kO$_&R> zA;EG7=D)eLM)$>T**jv#{@r`}cX`_B^|`|6GlUA-JE?q?8rQ)BkS0`^y1UVMvRG>d7Xx$~Wa&gZvC%@9r$vOXcv z!RmOtL$2XvP}=M#vBvgij-OxiH1wF$CY}Y4W^K^;zlUkHWJ5yU$DEVRC)1L6uHIDH zc6iUH&}(MBZBL#kHU8akQ;hlVuUY5M<_o=(|9NfzJoa>5ts4{_Thti+`*HaL9BiIJ9KXd z%k|4R%2dL5w(K{7&U-fNecHJ6#S=Nlo666Bb~RY{7sk4Pn&!;eZ?+do^zXXa|Df{5 zyz7Fp=r{rwU$^12E|x8l57@81Z|cGz{L=+AkbhFMkz zHDZsOsye*4nH(n_HaU35?$>U1_c#?hWxcKm%QzT`AP&>Q@Q>z1!)j z=ykI!c2b2+ym?uOu=lg#^612gevi1snXsY^Q8Sb23^U-D~O z-`A;AD<0nW_|SV~yE~7xp@HI~V8PjcqMuz3S~_*o+mH!cvW_k_j=j>-c9L>URKJn=!s=-O%lx1Y-M)-i-VS+Y$0 zVDYiXHFW|O3*$=tJs-JG{HXH0ROfKK#f`n)Qr|tf?_280{koT|we!J&Ngt|{x;;;R zeRB2m)5w`SPX1qI{`6$DO8k7Ym>(r6%GakmKaJh@psZ-AS5~lO+(+F@bsxT@zWiB!PO$HWs@}Kj z+57hv`sY|Gg9;tyPZRhSNq8;&eJJIcw@$i%nkff$L91{E1dE?(F0}`#!V%Kge+5Thz)G3$8gh zuau44=`w|fYrf8+*{QMWZ_5)!8Pi*LC*1ql{=>{-&nB&AuE`Y%e7tMr1lp(Uyqc9~ zwlTwX9Ye5M-@J>Czom}9W^1oE4QxNW?P0v*Z`ZcHr>ai=?iFNZH5N$q{EM{ZM@~be#(WSV`l}j6@}|%OI)vM<_n3vZ2Q(b6?~X0eDCm#T=!ja`?szRh)65t1=WbD70Ci^A+fN;5Sorl>gu1>~~Ko}IbYQB-pK z3&Fg8IYu7iqO-ZHUTwFO3p@1p&ywZ4Ry|4W;SbC{=kZeH-B*ES&)>GnMn2?W^IU!^ zZ}ZDbMo*kWZhk7QmVI(PS%K4ijpUr@+3|tGqEq+&R;V_&3+{R5_2=y3`;&aWXwLe+ zSXDmiq`9B;eb*fg#j!bph96T7H7sG=F#8_c9EEnF*$vAi^VNR-Szb83i0@C!a;Z}> zPy1YxlNFqsWRI=3nCV*S^C68b^Zx(8V&MlKtd$JxR{e9e!Z`36+qV}L4J>VQ?`GwM zSv^o%d33A&rLD>jJ)L6%-vr2Qy}htV;Mam0`_8b!v~8A^et$1MWB9}TVv0c8!2mUR z1K!6{&3`rQ&a-L$dw1{4zt$a#!hWCf)=AjaxjgPb<*_QkO-;YPtth{{=XUWFpO(9e zlKd|kmbXPNN{YQZX{CZ#W!Jx7I=|HpEY7RyP1}{XXTq%?&A)xS%)4Z2rcPwKQoBOL zaMGHol4jq#Sa&^nm-D(R(89J>;wQ_cS0CGLf3p?po{W^b`{C02-G|Lucl}7Dx zFS?u9yEK~V#S|`?t&_fXT$yk!B59Y9w2++4JnJ2S2Dv3kVbdL~ovUS5&d`$KKYD51 zhb1?D&R>32H*$^4Qu&tDsVs}Ci#LPE0t#-#R-rMW^U%qVk7y5^>&Oc3KQ{zpm z6_R!ORqDHoBA(sMEBbiku2W>gY?(>Lfpl>6hJD?=31p`dMxSMGoG;LgPS%Grzc zkL5-uHU8URna;y9Uq}Df*NYb~TPN6^`Vk;3tuM?}>#Z|6P)gUd;!9|zub-L7!kad{ zA>H@SCZ=A$GjI1xyA2&O^Glb$G&h^QQD57ReM!@-U5i3rzB`kU!tO6oX34VmQ(@_j zCHE= z9}oX;l|NU@5Atm|_;b#J8}k|W1z0>g$=rT2e||Ohwp%A!v{mQcZZE%U#d3m?cah|) z#dB_L-sRH0Vn?E*WA^{ZXcf=OlkUseYQn#n|D3zE%DjlDIH4(T&Gei@+xBN1%;4Q& z`thozyxTVRg1w)z7qE-gn*7(91XH)jdPn*44IDP>dvix-?*kB>c9Cn zF8_C@bHi3euRSw=hnR|F@%oT}_qOIAwsKF~;i+etp#1$`oR&N1r$^_-RzJ@4vAU*j zbm^n>qa8ADer-_m_|RUSEP8dC_yTs1eboXFS`}vhdD>fO^!ACU`!Vl>`)7qck91qN zQ0DX@ivlT@MM6;*I`z}ey}W9cq~$gzC*-Za->I8sR#VPoSiEx9`Cs%+er4fjPzKvy zuN)|Hv%+A~t5X?4*LP&?K7Np+=Iin&A&(J=6jd9X-rG`qo=Ukk<@8;9L2JG&CZAMdltKW;NiA^s;!>QwVf+jvDoC_ z)f{#8Keu-FCAr-3jZoSh-LLak=5VOYRspU>|IcUsRF8l8O?l$42dP@IyJeoIh-lFI)IKUQgI=vP76U_I0GO-l2-8 zmXGuFf}_4}SkC=9^q$?3=*I!)^J6@p{XEAw?}wW6t=n$?W?R44-_&Xp@1AnkCnc;x zx+!F9wx+D~R$=$v*Eb%T<_VkYpVZlZ?Aq}+tPdtvB&qzl8dZ}MQpY(VVqeHE#m@{O ztSA0W-rM==yeQM0dyW@eI|R_Ct*c4GT19d1sjkiK$NOtNHYTyTP^>$1&FS65DHo1@5+xw%~Bn&Q<) zqgv(}_X8HxKf5+r$f4?>f9)NvJzO)I81zr<*rD~5!T4``i+Q|Qqw8nG^w^yuX<^6N z)4%`e{?X;u%72(x9^9`KKD=!5^vP}FWg6%I%WXMb^R;Zrg2`Lr zr=;I&GPQ`kE&4i4T{EiS!^2~>C9^h}S4P#(l1RJkcWvr#Vb@rXJB(M7v!0qcZu~oe zwfLj|_=)Xhk@F`HM&wPO zFS(lM@>V60;$_ZN{cbED4}b7%TWKS6q&;|ggS6UWM$xvnm-r5@Sa{HM`BsfAft`(+ zZ*CnmUH0JFl>c#ZMssF(H{4Pda*o>KocLy^wbrhK-=8e{Exd_sz2dE~x3%q8Gc+7w{jhS6q~U?j z#+s$xvA-)GepL*z{A4gOTC`o}zktnKf6a$Sw4xGc@!p!fa+%WCuoF=iC#(Npd{(|g z-ccs;=nbD+rqRw9^i%F#J^tmXT%lyv_LvQUPk*e)eicw?Q`{BvX-QVcrXTK^y*y28 zw4Cx;T%>gt*Idb8cl30R=h~W<+0mh|zj-Ajo~UZR_2t4s@w1mKHP7|;Zd)24EY@wwwHInuSiATr z*WcUqphES$P7rr(4?ab^3wZ^;5n@&3G`sP{jJLi3H+~!cq z{9PjH3P-N@6o(UKe}jXI)8EbYx$>un(;?uLjG6L>c#+4qzRW4?J^Z6V(5q*H=&nCr z9L)LaCLh|QXB_tWMyu4H2&uzi&F?=)E)MOqJwc};U1F2fitifz(FyNwusRwURCifTJNTf%GwtoC|1tl*&M&#;`)zH0cx>qI^QUeq z6!XjPz9W$NGvHvp&J(Z0hpyalIQJk*E!5#X^N-z|GSzyef^;^mQ=O=Pz`x|d(Nkh` zlIO~9T{ZiR${9wZFX?Yi>j-@JUTLd6;h?#;=HYX%yQHF4q@{avop4?h8}7 zu)ZsMGXp(YcV2CN_jTIBL+MPG4ShDslfoW*ovhjyt-$$F%86a4=6u7_fJ4vx-^|-S z&AYU((2h{bkG97s|G61rc(y6eu>VlU_XsWy`yBn9 z@{-(kP3`yotyS(_G{fLt*v%Q@5lUPy=WCQrc_Hgp^s&sx=6-+;cjhjcyal?`*6p=U z4y!PIzR9zneeHyGsrz5-RomD0XZ_lR(HExP?Q-j~o_t`%@lB?2+I9w3J!}F~Y=!Oz zKRx&0zh&s8y?ft_xw4&hylpu(<*rDpr1xYzWGR^i$(d*)v)k*jsC*u$qTN0vsw3?Lxu2 zoLh6gq3rWs(_P!6R%^F~9KPC;#^koRoMCsZ@anCPuN6J6EIEF0v);lZ@+@<$S6Olf zTG_1Pn)0OJ>tmbc)pwjVLKYn@_*i>Dle6TYwXwczHe*Tdo6bVjCI8G;vAw(<*l1ec zeaViI!`$fN6)V4-)LJ!B1wW0gi`WnU5w*{))t2;J`s~+!k%l$jXZPHj!)fy}x%OF0 z&ehE|bM=&`i$zMz1w%S{AuPEMJxFoZj{ z$EVG5-{&j8IV#`hVyAt1-tvAko(pQ5J)ZL_$ksP0?vD)IA~!*H<}u-QSC1@Lxx_lB z;?bu3>$~$$8r__J;b%ibinY3h=LWA2+lpK!8LkVR_qzLa@9sXvn2r^EJKniycIC`T zcamkeUo=<2`jafjoFCSEeXm)>8lT(unf20D!SBgU4H?oZe{M!^TEE~`*_)e2>mKY9 zDCpoZn&Hj8?#+zZeJ>}i<>&j=d~dRaYvH}EA!|+YOcw2kPR;aaZ#3*aIkgHW1%N|CE}Ot;r4QxSuxqg{i^rslx&W-9qciO{8fMC8}KBfNhLCpJhi+LAaFHa2<%XxN2x+-?N?h)JRtNxo$*0?pn ze)q>o@@%4-Z#P@d_$5`O`6)~Ke%WmP%qAAzbbVO1HBQuhTngAOM6Cz|93YW-FT?O zK~nGal5Z7?D@?t@?rn2fJ@<;Gx_L6?r>lAysTe4?FX0aan?f@&+_zsaoJ_uYgqLDdZOKFhOYN+wKdic zB(olH*Bp3exkN9s{gGpIVnc}j^~P(lMu~=X>mRJ*{$uw-Z0DML@B6}qe@y6~8qfLe zPt&^Jv$lpCKhL;+^q_aj`D0JluGsXIV}^0X#;I!zA1*Ox`S>{WMXeD}kt_eHBTHYz z%vt++uL#eCh`NrW>GxDaBjZ!Q|4$9_Fg=xIa8>2c)tvBEjYenHgtt89n{E8p?qu>j zW{nlI7qZveO@7w1aP8@cjY0QcNS3X*E@oVM{+q~4m4dS8aYo`|*H1|Qiapz2EqnLn z$H`0%(KB8xJFDP3GhOh<2i=;q>b?>&b^aRZeG8J$+zr^W>0bP!OQDS`W_N5}9R8qf z&bE|GhLaB~mu?eY%e~Q>q4-pp^15|*`S}hRAA9Uu+8QdJFiRjf{^#*OOzci=;ni*t zfeiB$43=%&p4OMjf9op0)870oCwwn69+$nVo${_#%fkNBp7eb8(3Fpt4wh;BdCeI1 zcMZqn?Z4kAoqZ_%XnxU~JM0N7BrVTQ41K`-xo-KD_4A6%4PVcWwo_vGO_eg_3O4WAgiom9H^ z(F9SO1YM>_tFFD-DEWY4WlPCH)fp+)C%#Ve{`;Zv0z;R|O7<)4A_r>^TUOgCo_66s z`^(NXO!bA|(sLKnH!&-xAN29wqwC$3{j%{b`?Obg9_{OY#rV)RlquP5wc9i8Ej8Z` ze!cdgcF%RKwSP{2DhSU!TEBy1`g9JR&661#W;F+;uvEDB9%lR+RdG&v(o3)S`i?U# zvy~r|`s`*24OwYy7QE@nu~~_4cK=M8!XEH=m*wHc7UN3u)NW1@!>c@h&6X_@eVaT* z`CzunpBpnn)B^=~w%$$Lwq2>`eHKq@5=S*p-wu}p)3c^dh_!w-GdINfmp*@pYHveG zet?f3SMfQmw?S_kd=BrItiE3N=d5JhzSE5dif^&^xgR!WS|lW$llObZ;*_<9EYrUG z{AuL*8Tb04kFLYayxV?L?|8ef-Se2g;^ot&j~rj$y&qI+zT{;Kk4NnEfIOqgbA$Yx zc3s}&IQ!>sMZZ0jJJ;L1X?5PCRH3%L^MC6yfyg~uw>T%cs^2aBTPylvn}kIPmzh%9 zy1t%0Tcj12R(-yIVfrKHHC|^P&HZGq$Ix7pV4|fj_uE?638zB?;;GoJ{AlBvj5hsy;>e&((t5v-V*+ ztC_5yX1lIlt1NVMXW!%aznA{AUp;eT`lIDh?ECqz-a1&|Tid?URpgs^kYeGyo09MP z+$&$VtXMb6^ig?5-l5E8<{#t#e-28vKL1+7TyM?Ue^M2N_e8%rKa)x=@qf{(^>X*!K-@RYCoUgfJ;?mH#pQ}Pxt5&KC zCySfLs+5b$MT;rjcD?o`&FLi9sh1xu_;z+*$}f+znaQwn-kto6GyZG;P6>)|6#qWw2M95rgX)z4#hoQex{KR+2qu2Kk_i=pJjX2bThAvaH-bG8TwHkdJTuo zc^tR9OZ-1lfBka6fqIs$rhac!{#;}J?lN`XrngpXpHHp~6-=*{3*a+vHJ`?^g;9DZ z%cjFq1oGE9#rMW@_a9!mId|uy-cs8s$KG+X<^}aDP`WB6s+7$PI(%iD}B2{Tly& zPuOwg$JVkkZkfvrugz>fELjm#@oKT(UIv}D=2BG?VuBalE}YH%{j-C6^%pO9JL4}q z>hGPb{T;m1R?u2woA9FBt8$#rsGjvY*0_3A@4WO(t;*Vu$&=ox_vkV^1u*ST7i50K z!G5yDKq=eo^1-J(Q$GJb_ERC`_@;%&iyA8>i%s&LJi7g`X5W&^{Uz6RCSJ|jH@)~J z=bnVT8c?1}(MwWZz}Qkb^LG|I$Ccch0{1LEJQ#cCJo7DgW)w_lBh;hD|4(Pk4Ao72)>JFZc_YVT8~zF>0+w`a$0A7w^f2^QuhPC%NJSYRpZ|GK6*cSzr-ZFFSTxhZ8oKM>jHBx zH}=IZm;L;7TBUlBz&+XDDmAg2SAR3I`MY5A6MZAC`jwoYr{pe3t1_GCF^fN-|9rEC z>w9fbrB$6OrYF>Rr7!l3k#eB$k>|1(cQ-6ypO;W`%31zz$!Ukro6an6|I7Mhx8m9U zqZU#;qWo4K9@jn8ZTD8y)aFTCx_tjEY@n(RH4L=8{~UBT}v9hi`rs;K4(vOpl$tsOJDTiES4S@`%NnIr&r|I zMJ)a`$vWfQo3no3G)u#jqNiNXjQw_bS@*nS)mub;;|JWHXqiK+tWWOoUdW(rSzEp%5rNPSn^p}?$`ty z9^#V8n(wiT{dn<}`G2==kcfGcy6SWA&%%WwVYb<dzjeP z><-i6;F~(*t);E~?9_L^tzd%T8B_D$FbRshb1lV2%K6YsI_xO>(|U9 z7q>T`o8ZEcQ5*Je?#BtIPV~*oVt!HKlvFxRYu~*0svMJ3Zt4cyZ+nunR5Xd7mC;weI$Hs8u*XpdAmOZckiLK62 zO`H1ra{1r+by2q;l=;jJS4vR3Ie(`A^3!)~p3ExI@Ls;TXA0Zo$yJv{7{)l{?xG7VMz`;1K9TFY7g>c8H;)fr&aS;C_{rqY z!K)z#-%oISNMu>cv!Jl%!d%U;=L{=sPOX+%^7_-^Pm@%6zeVNG%71q*-bnq_dXBlq zkK0z}6x+Y7fAn(djEXsbt6qP*<#4Zc^~5RaJ>0?EZKWOdGnCrkQdnSVw&nnbs?t}EOP>mEcfDS>B*Rd0 z`ahNRys-s~Rx|y+wWL-4$5);2N7mj}dHm6^C{ce)eU%*ZdgGnbEv}#0w|VC#U8S`W zyC%#(;&r&`+U*HbXH1{XwSaHh^s>#1dklV0HJ(slEAzX>X14WD_2sfhRx9RYPv6bY zayN7Ly^r~CY~9trCe4zZ-?G4KRpO*1ub3;k9J5az_B|L@d^Pm$9L^m_7!Dj)WA8P# zS`xy_A(VE&!FXCvj>@eXo1oU?VYi(o&khu_;___@^N~q-!8rYtqQ;Ie&HKxr9E=xL zYr5_ber~Vf)zpcKnjPGQf9$r&hA4S)KCNTiDZcmci*;VJeAW6p6OQxvsw4|3?75`! z=W6_&sjAD|IL-S1wF+q)#hHI^S+5|M(6i^n?&pS5cO3R5?)tf=JH|0;P0Q=tB>w#i zjvd`pqOsfUU1q%EwZ}|+H=bBNHo2luqxpVL!k^&NE*FnKN+^{-aLaPrP4kZI1uO4~ zhbhW3-j=)YBsjKu?p@cVE7>;lc$qs)UC4D!xarK*3#G3wn|`QBRG9drz(mt(N8&o4 z;%`s(Opuh z?$e*tGWdCTZxhjHdaR_!^5OCNO^N2Om!5sT@TR8nJJm1j8y@ZrF5gqH*fHlZlS_NH zb+CTpk^=wCnxl6A*|tBK9K$~+;6O;pi?0o-d@QYs&Q%Jx8+sfMWUt7ZS8!ka%=_&p zm(NaJeTKvvp0>v)JRdQAf8)b#f5L0x=r!-!-`piF;R`)To;O&`J%73ngG9R(p5qm;W zViSY$pQe9~m-J-vSMjBvPKwfcWPZqPpV!q^|Ni(-2V@!V{^H9%wNCYT;NrAqVFrFj1N z`tQ~rX2kWJBIO zu&8deS+n&hYs#JIzrxko>3;$xS+*=%Qeu;xJvlkg^sVIEw-x`ociSj7ycbiC{$p}Z zNI;#%`r;l>jDIx6!s5iKM_C&utPlO2QR%D~<0ffknxn6%D7xa?;wHNfk3yLJ zZEeDTE}J}`H%dY1n1)nttb-Qu%)4do9K8`{%tm=lv}F8pN^whpW|Nc8xlzZ*H2$X5KO4TNlrAJzQCS z1#|s7frgISFS0H>H%Rq2IPSi*Kr|%w$)io2d};x^mClFf{$BV-Tw%KKMwc0jCCxd% z8Lhukx*-0%PTI6%LA+vT1NW=C<*Iitxn8QcwcllLbHmYxXT_u^%9v-~&U>jJKRfVq z?ZsJp8w#JT7qfctqSV2a<3O^{8oNmxpY}O9Wx7SW?wvPjh4eg*Js%y~|4wjuBJk{# zk?7{>$3%She2wBdew{U?!);HIoRZFemGpH^+d^uS-(7o^?00NoR`WWW{jZN&?Vitd zioJwo_tdbgw{mZ`h#pOT$G7tOgx@mW8`o@_)aA!E3)JkbY4Nqp%#5ots#z~2p&ayd9C94m2RcM(Mol;yIvW&lepXnZdzuog0?{~*+jX&3A-pI8%COT?Q@ps*I zeztQQ-GV<&$dcO~F=cjs(wXoHZcno(J2$KRxiNRm{pB-+xmN{CG#f4Y8?U6t$1sIQ zW20;!>y@(>yl0l|b3XF&xP8Tb>8!mytJ{R+Tr$^w5q+^MDCU27>I3lyRhwmlejZ@R zmlt0tZMi{hX5`5)YwXTxJAdG|S#>Ys%Y!F3ln*^DHq^8_`fkDwwqwtY_KTRwvE7Nx z%6AG?O`15};s1(nx$y$7lB*Uka?A-{_-L}Ma>t=}Q`4>-J32}Ie#+`shf>*G)f;B} zivF&O5P9O$JTp$h@cn^H0@e3VT)(1yI=pW0Ho?NYH~-4{KRM_8xNywx@T98v)pr)1 zxGB7S^DEx7WuFds1w6HC`RO(`SRzQeJkJ$G*ON<)5`r z`^xQ>8>%5vPj~P8zVviXOt~{?jJW)m^pST?o{BYBgZ!hIj$M4X_FGA((mA%Ch*ui- znO2_T-KVZ(@_y1igSzuPKN@EL=$#+-SDKY)S7!N|!rC_W$u>bHcFL>Lg@e|c8Lmq- zo^WkOox{gzEG8xzH>7^+Ew~@8dA(83wRZpPxhFUBUEXM_bExOX{kZAr4G(s#)>Bdu zemNsMT*z~YRP!-kgBb6iYh7kF2hYFR?si8mLH}26HC>YGi$zpw2~TPLQshB2Ib_q0cjRY@vJra^m8OIO#JE_vX*{J+JH zZK?m%pE^0reZsi1_|9=XLoWB=P2!7=$Nq_*Ch+rTnsu1fyOnbrOn;j$UR=4o<3aq+ zf;pE}{#=dBd=wsIw~f_1_@kRuw%<&_nnlm99-J-m`04%cY8UGTSbdMi#Xe9eQgT!b zI^1}WpP|gZSB{}Y?fA{(v)kq3 z*Xv)9_4;Br`QNMBiTCc6DB0iNB4o1rRrg=f7kAm09_aSf@7^xHICOsEo$A(w>zr8P zwK@_`$UVF)aP)}Q-@r!6s@NaJFOR0itbZxJZ|$-r>jL5$>T-YF4Ng=0p>;-cYFz&D zwX?Yxbc^@@Wf0xBDQ@?ynH4e@{1(W*<_xuN(iL1^DLwt!;k(fXI}6?kn*NokHC%3< z#4vHOL?u(plJ`%0PPA={_&lfJKc}#|}?joYLiu^BGk)QUtdA69Lh z@O}BMyv!duJO`&oZ9i$%J4@ct_MIZXYrx^ff}Qu9YA^I8bT9}9uA8&xZSkf57aCOf zv^yMH6ItEXZ)P{=NPif9pG*DHPVE`LM4qTrc!gigtLM*G^n9=T&~47Nsr%L4QlxKZ z|2lEn@sH{Kx~5grgEQAmJbWf{e;)5=rJZq76J*4CJ7$@l@7*vX;8LBQ;kw&P;%DuO zZj*bFezI@I=h=U5uU0<1=-cgI0U{mu-Ilz`d&4cV2C`oF)Z0sD3nV(9Ui^HSQRwL3 z&ykuAt_~BnIULkU`sv^{`=N95-|&F%obIh&bH%P$32w;KkkM|fTkN%#Q?j-_f6Z+H z9SQC)EY%xJe@7(R9ICk8T=8Uv#|ADQBW0h*Es{Rx9v$#`6nSa-&gECH%oI7bVb6YMNHE1zt}Dk)JERKs#-c2xSLL&w1ZWQX{8)WO#J#>yh1eBlGVt z_9%wkSgq)v-4h|uuwKUQtfo}nR~A=_0ik}FM;``vir{Z zi?x+o^D|jYHMi6LxYAsxX|?j7tC3q)uGzr4Xam#hkNXqTd-jDo3X1#XuZ^1FU-xC^ z(OI06QY$Vz{ktZ(c-Bt$YX{Ez%~s;9oKW(lWiz)|)CRi?mv%S?m}DK9mo1l)9BKM@ zp--;_Z|OCaHwQ%Dn=wU1@7pKK{rVYu#iakKON_(jFMPg;CE{vq+~$vR`8$7p>~FuZ zjPvNiDHi%t%YGNWJ}ul47ErGF?33<8{q8$TGv)4NpE=0&Bkf@U??X$`TNX@g6Vj?q zf1Q-IY3|$fj?K}h8(#QrH~q@KqI$u?xdE4i&YodWuat;tQgc)#dV>74{eHiMM%tVh!Ae^8OI zn6>VtEk^@GerLqy;N`hbqBl&sKKa$|CCgt2FYw~{Ypcld@!O|(QQsr%+DbPi8$M|y z`R9FoJ$L16$%Nt{aj6?7kDfS95qZ1NTDW_;-?W{(3bk9--RUl5+J3D(;q$B|hwtes zi>_!+IykBB!eh(XR>H5#Y`s?Q_#SgpHqy54@!`&ukJ?f++>)e{L^yl||2_*4nxSL* zLwef0Tg$IGoZqd$8zEES{Oeifbn&0>6*!?T9wz2Et7ewxxx2(Zny@$k%4T6QMEL)8v z!jAB^Zp%=4`?70MdyEuUFzaelO*6Uq&qVSns>}lwi%k8Lb&ls}?LVZ$V>@%(-Qn=S{#luNfON9(H&rY@6ZXuu)t%an7!iQgNPh zw=YU3ELp3r@BEz6%3<4^TCsnzi!0Bn^PKp*sDuBwjZALMJozQpu4(Byte?@Vd*-B= z@Y}vQ919kHjK8OQYuV}4JLwO6ug!U!TUF}$^SavQEY7ui4j8WY`^^x>H$Qv*!~WC9 zOSx8G{;{v{xS+JkT*m+Fg3q5e*tbXJhs(4LyjByOCaEg_xf(JfwOQCqBtd$iOBJ(2 z(}91}p2;j<8@O$A?FxlUA>ZQkR+Ud_yvz0=rs@8ae>#(GpKw0Roy%E!C&EM}{q?aL zuLZKtvu0fpHvDw2x@P|ui%WsYwoW#?S9B>F8KxcJs?Zmj{($|gb3yg0fAd|`^P-e5 zX@oA!%h}cC5#Dgu=faii>m^xhR_<9<>6`eyF5uR>n6fwX{+H?>OkzAbC-Fe}6SK2( z>WY$=&(@TQhzu>fJ@+%?bvEXo>c977<@1~qR=WN?)gi#W{^`}P%6C&=u!gcYP5-+^ zX31hFmFYd3&g)7w_}*VRY4W4I`}d~`-!d2de#G^4_$0P+ap9-Clb&BV@>u0v^|p(a zYgoT%Do%OF{ocnkrHt)>%FoXCQOANoUC(71@&Xt3)|^-Q&Aoc92g}w=O@n?WDI5^DA?{8&M?b@}<7#iuU6xt-WEr;`78 zVD>BK`W6Q6H7dp%C0;)1jbq^}oATpV1M4%kr*q~NRGee2V2mnfjT|2?qabm!_XCVL{4g#Z7H?zz-A-Q@9$@0WK@kbANsrm2#!m{?B= z^gBE=cE_D`{#lEDG0D7rx$(0yWZPkb5fcLlyqwTt$il$Dz;KvRwf^({FZnO=-}pc4fA0U>|JnbS{TKN!@Sov7^MBd@0{{8{*Zvp& z&-tJEKi7Zu|Lp%c|1#8`!Dj}=)cf^{r?i6F~_bjrynZc zm$->vEA^?F*?If6cY*OkJD+Bk%L%^k-h5d6Wm(6!TUKkIiho?le_GBc-O@NYe}8m{ z=E3|pr#<)mD;KP|v-rfPSEjilW?swIaV+CYxTn^2Vfh3l;mRb%)THk@8_tJVi3MeO zD`aTvZi@UX!x*#AMfl1B^~}Y*RTtLnk@DY@o)ge_x<0Sz2y<7D$NkKf^hpVW# zoUQCja*i^KaL$X6P4tOqSE+j8c=JWiF>l}d=geibGuZpYwO4mmAE`63%u5u#y!ar? zGz+(hE)Horo6nV=agu20cCc#^dJ|&h@LzsI+XU0e9f?ccsr2Q)d#7+rI{OjRvrkvw zX!>hxopq(jut3b&{`}-WSuX^-6xIh?#VMbiKQq-BH2k|??pCIy$2URH|YBo8M56s^^HJn@DZI!LYKEbc{HJn zS?e@w6Pw`-?G3g%?p#*P7uzrNR~^j@{Qe^5;QYKBK%XLdx_}_wkUK##P6stz&P$N59VID zJ5Fsz*0D9UUJp4pZ7GsJwJuuu&(-+v634{TJc}mXyEb##wXa$&`m0x@Hso!cWqt3- zL9dU`bM`20Z96-u#XD;20j(qha~(b_CI{h9*+)J;Z8dyQ@WYL7^*m+%ZS0fuwVtF; zy7t>lNaT^C#p5$}OA8*XHwokac&1s~bG=RI#g^@FaDZ*Q->vE_EMJAw$~Qd z%i@Khv^qOthqPH{TKi2>9s^_bU!1R#1?N@iXJbP~3z;yV#+NNU) z;rp)1?6>zi@TOV1T}0h?>dE`91`~vo?us)NI6YXZbRo@yL1+HGvrBIGWvA3-?C+bw zbg^JHXsvl=ip=Sq9Je@G)o(j|I!nV5$A_&ul}`t0p8T==Q_Y)*bNlCsna50; z%zm*v;Hi#BkbO(|*H){e*VZPnYn{I$sD3EeYsZa8{n>{XemwU$e!ZwuLfncKJR5@x zPWaq9vHQ~7kMlq5*H+kjNaxlod0zqXfY;G%=3i8hT>U#T`Nn8dKa<@n3{4v|&|t(s1T zv21WOczx!Nyw_9_vxci@ZtQroX{KG*_uj6Xesc{gifdh;9Wj|7z_qJz0Z-K4{kGqu z=E_Pnvdb0C36|7RP`j&;u%$iGux*pms4%hb*u5ifQ__DFL-Pz^qqhD{l zbYK6vu9Tp*GiJ*k9-yAmov2Sn0KJGSI^7h%a+5A&J4@%d={IH8k;~G9dv+bz zBww`iXVkGHGxYS-JDs2C@3YqrH}!PmJfk>YPh3)5`%2CqK9SZ<8Mj&TQ&wEq-#KOS zpRIGA|MZ-G-zzd{{ewL-pKW25aDT<{aPfCt6Rn!;=S)n`Q?sg%xrJWLP?qttw*A&~ zGg?R_Hs9*h+l4DO{gU;(Q+<&4oQ~LiyIHZHGn@XZ*4OOKUV5CeD2#B?OD!Q zT%Rl$yp8SVp$C(d|J;~acAT@7rEtBPg6f%UCds4X=U?y@<$S)@cxYF%@bf22ZY&au zOs!s?>$ylwD)i-TZ`Z9ar|b}Sncd?nQr%EdqQ;_rKVtFymw(@VyEBbhRL049M%tnW zC!?oMeA{Q3yy2_4$EU2`o+&4G&LU;%vWJV#shG_559i_c+Z1?v!|VraVMp>>IXh+8mwn%I zTx@S~VtP&0>AiyTGYRKS?fdmD06pkW6?eaXQtTC7C&D^uab-37t#ByN^H$hoqdYQ_iJ*u7&-ix&Yikg71R%i zRhP)r+!1og=4snYdH>GR_6^OJ-iQ45yucuzOl4@3E-jPzXa^=o9Ygf(^m{a9&PDW;)S$>+T z#lf|VoN5Ja#}f1!tFL}vr+hg^aru2&2CtdSj z`z-!!@3F$^+U(SWw~lSmyP(5v8upd7$yr_diE@TF{v>9z@&_fJ=&r% z(-qg%Z|>V3S`sr=mhHNR9MfS%PX2_p}9OzV^*7OXv+Qh&!uZ5RHnGEengQrB50DUSAAT~F5Q zSC!xOTA7gBCjWER`jYtkwf81m+{~KvgF}3^{Qhq*W!UETG`9KAx#QEV{OA5;OM#UC zTb>;eddc=P$)YIbsjSbA8MTh)qOm8|FOo@HF8I~n;YQop|KIo81;pRF7rER@D^1hk z-^@LpGOo8~uR7AZ>;Ctyj_dsw_I#KU+IC4OpGi~kS(V$(Q~&RDYJ5!-6X4i9^_A4= zQb7~7GqD@j73pVB=ehbt@z!jQ|Hj8I?5(o?m*cYdzEoCnm+`mb?gdRY;rVf&E&TtB zUyNKSop5kx&(^h`e{L>5x;(q~L3?4*{dCiulQzuTb*F|L(fTL7@{;VYr_WnT%2Ur6 z*UWo3=i-GmZzg{eQ}h;=nEZ4Sr*wl&`a$lB{q+U&I{2flle9w1xAdQ4Ke|$1x3qcn zo^?B7!_9X+`tp10r-DWQzxZx!lZ;!{{BF@K!yNu2*Evt=PiMawZ)EoS?ehiCLpN|{ zahYY9b{xu!to8nUO0n}~e8A%U++viOS5-&H_tEMKr-zG2Zxs#3M z?!Os}S4@iA>#3~r=h{#0`4di0h@Cp^!9?x(KC@K%yD!Uo%=_?V`L^k*H50D!*iY5{ zWZ&xJR^DR!_PZ40!Rm5{Y1PY;T{>QVyRG~3jo0pmPp_uDJ)q9FF?wUx!!jA}r>D2* z*%GoBwT)R*;3{-`Y17#UlLQ@7qtOHE?#QLy4Fsnk`zleu7R z7coT>Dx_3q>TTdIyR>79N*{$H6{ zbMMf1r{_P~=9F;DEm*PWB4ZI(o2_D^eUWovcvsWC-8uIhU;aH;s{8!+zM#S>&-Uu} zPV9ahJAY!j%I%6Krxd$bwWFEw7fxRFk&&1F<=0}uQompRUaL&hIkxq)8F#GO7t_*u z>pPQV+$ZJUW3kt+TW@EMn-RTVX?NMalG*D^qk017?GV3y(JW(;*yQa&dyX!hJp1__ zKIXKCe8T4pj9x~6TlQU3FJ#$QsT(H+T)KUja-yF^Eh>&Yt^DV{gUpf3tbN=$PhV7K z?%H?DSmxgL*SoHsnfdwDvG;7JWovWi->Q@8+?2*G_2N@l&~5&{q8RV6ZsFxeGG9-b zJLiMQWzmgIEWckqar-;lf+zdwR2N@~)G4b|wGObllqqdt-taa;-k3Q=ILyEI=y6>I z$87tpEzWV?p@mb@uTSW;c{Yh7Vx8Fh>h@WCE_uDLsWkf#^xEA*d$!2B?Y3Dr+t;tu zeLi!U%j*qwZaOUYxjC|AZDhlvdt9xHdq3B0*>Ph1E*9xih3?i;aUXk@AD*yn%cEIB z2W?lVy?gOXFWSaGs8;S$R;h}?x|hMGt&8s7+;iY>+ONrNPdQeLd@5LW?^1U98_i0N zfNZ`8uk({KPcmFJXcJ_;@@ht4^}e-}kF8afi_}l#&@uJ?D17qL#?8`qj=k8aA;Gv$ zl+v=-?mxHMaMExVSl8ozU5! zx1~|1M)9S(xw~7f(l?&#SN;FJn)p)Z-E8Oe)(`l7td72OJE3zT_eVrtN=nW(d28;r z1jn*1f=Y>Z{wI}xobql;_dmXp?UKKHy)SE3T;)1+s#-lJzH-J_`-**U*$#D_nH|xcd|2hrwQUh; zEvF4EHx*p=w^-K69pShB`0~a_%$d9oRomXql$_rj!EOF**5;WG@6%`d{r_&*;^Ho* zzU2GN0|oQ%zx_N{a=MV#=|8h3EHv0>ax9_PVDolSW5aVMZN|)ppZ3l;KJRUz0RMma zwzcs}1!p|hoxSXFbMrmFUB_hI9gluWRoOgux>4;?!Is5|Ym?t}UTr?VUOw&k)^(vX z55N0;;BwIipM=>eM<;BY)*n!_*n72hq>NmMbVpb@e~@_K+(R818NR+&?LNouTl)Wy zJ-YJ4S#hP$Mvvu}`>fghZGwW*!hI)RZ<1bgj_c+0LXIA-wg?ZYKR#Jb!}-pc(R)s+tx z?>@Qx&@PofS9AaQpPnDJLtlgC*ZL+V<}W{tru>tt(tY7^TP0=pykqu@%J+$8GO-Kr zYF-Qo)r?-u8CmoGxAf1szrM`<+jE%bzRJ!1o$<2wUWSAuiuB}l&S_2yKhRoHJJDwO z(fdM{d(_w4s4Z;DlACGNt>IUcxUV-n;LihJgDH7g%7@vQr)|2zv3c)hrcEin`%T?b z0`AO{QQyj#+}$57a^A9nPvgQ%*Z!(+k+-|!lE1#IeHL38x7I*6cfDEvC$A0Cj#(GA zT+0(D`>qr}GU1cV1Iho_zpapK(Rt&sZo^WAtPh^+3Lkx)v{rilcFSM3ORhG&I_@K% z_Hq{E%9g*mD|D~Ore&^Ozgvt8bZ|?Em?ZDg{rOLC3s=8iQ@fo*RO=PeORmKZ<8ASTj{Lz^uAC!R5o&1G&{+>wTU$DV(~n?30Q> zUzOnY};m%aw)m%f$i?^&C;UU1NAeK;B3BQR@xIX}vNU=YtOjUiqsV_Fv*( zJ40Ojf)`E49uXCQ2#Vrpd+?l_7CGQ-=iIe=LYYJ;FM^*D*_+LDIwmK)ncfBXJy|0t^M!nzo$Y=7- z4LnjRe?&L?hfYuV7VLMgbyt-By=?Av_m{B<-{%inn{ePtK8MDhjZ7chHm{PfT+ExW z{mw~ktLD@5;+Cf>);;}~{$T#W`yW>B@6%cJ^Ued|dojjStG50>GTq`$yvv%2pDZ^_ z>7RPeu6D-BJ&UeQ_L#Tha*s^1^3h*q^PFtlI&ANC_uaLd-~N@0-P=!K+x(=xEgtrt z+s!xma~TR>Sl}>oe`}ZR@pqri?%disNpubO44+BHa zS^a5=akGTA6q|W6c8VLtr++UF`ugY9KYRZ!CaJw|Ro!BxQqsP!d>_l4Fm?U@zweBn zGrrxj;6lZn)?;F485Y~ieR;^x;3f3K^CHW%hD`EkaTW}n2bKWNnmSR3$Wv1e=5LhT=wzJ6?l6R$Rx zmVC+Xo~^63Z{Cxd*30w0EA&p(KeM#;uHC63hTGepnrn1jnqx8TQGl#@$o~!7Wg_f4 z_LgZM?AlRoKP^roVfL^0ZKgFTPLgvRer42@WUM=Uq*7a0@U6&9t7+$>?cVQw|17Jg zlV!eEX%q98^>eIlU4Qn`_?OQf@4si?YgD|}cly=!Wc#$YOZw!F3EbSW^}wRaoCvo+ zZ&ZI2&9%#wb>F%zK=wD|v89ayq6-ySuF1?av`F%5)p5DgA(^paqwZ<$cS6}`txm36 zoF%Ti&?wMBrem%AsS6%a2a}I5l}(*rc%kr}ztg8b^Y?E$!RxxNT1hSN;_hhX??ruA zx0Wqa_^o@Ux#n3_n()l0-#JA&&y{V>-BHzJut7a(X_!ajX@QFq_g|=39%lS^Qzb{i zbQYy0=4Y;izSZ?u8UJI~#Ff{6`OUw(`rN*2q6%|r7&bC8bX{Dz?BTU*7v6gQ7E^6D zed?S&`{U+M)>HVl<$b=j=b3_$$NL4>rdzENKKbbKsiR$U;@o;1mOjgt+xfF=zsV`4 z{L}NqxnIGvc0rybjZ3SJm^*nP1*dx)i@l-}%R@3(LdR zvYoy>eR8R&__^vtclDs=3xTIA>eyY+Nq#=NE`8IYU&b5acU~2F*VZFfkur5f{rgW& zA<7>Pcq?A!)KFu2T+-Ff>fPJ8igB%oWl2i!;>?ER@5v0;au;RK{A1AJVtFRf^o2u= z?2i&Jt-~{SyyIIqzcW2ZONVcgQ6pnea{8$zsd$N))h0Ite$5L^e((0r>Uxg%WObF| z4>??}hfc8hE#+Xoq|$Ws-@jjRRx=oX|FEjgRnbv3IOHi)_$qkaBpD_VXC?cG@udw# z*IfU+@tIXH*>2yp$bCFd^sFXdzACv>e95l^f4oxT7XR7(+CTc$8UYElr}inoZ~B_g zi*brsesi+Al1)@+%L~U2m1~zQ zGr3&b6mR|JWI(`4{)@`Twz%^*g-`ocVYTbmWv!OyH^R)dCZ50Zi1p8`Qk^aHqjb)` zcG@|$^>E$|*Jr{nLKhqtV7Od3RpC^2ko>IVWjFkG8H-0ASbdfI~4}BNe^1Q3pso>4T-=g|+nBU%tn0DA` zR_Ao{*I(vwq$a9eyfpn8FT2Vs_WUQ`+RiX(uPKkcbB_Pz;@8u{5(K1K&gnc%d^&Z7 zqfW8s!s_c%yUtEvlDiV0)V}$T!Ni5H)A={d)x41zvd{3wg9!~U7aPC%CN8?E-My>6 ztjgTjvq`YOchl4(uc8x#Ro>6I{3AkH?W&4lvU>RUC6CMY$gyAO|LuSJme{9HO!oP< z|5Pq$_CyJ}K0oGkef0!UhO;|cvr|jk1FW(yZ#trwIs3R-e8lExhAx(C4DsImOC=s& zvj4Z{dd{?Y)@Odd(^_A)U(LlQvOZhG{m+}ax;6K19??m&4cWL#(2f1_?gdpDwJv7=w@O=sCDO~H zzNcwybn$z=yx^5@bKNpikzEb9BgA$txBA1eBQ?&c-m-P+%i`5@8CYy8Ps+aWxH-Qy zEpFS!m_0XdTgMg}=L_|(t=N;t>!#57%JI_E1hxNMf0L%hyRFgmVO~;r)%UUPZ_iCu ze-BUD`JDamg4E4jk(+1z`>|BM%)n55$2z@RF5IvF9Q?4y*;vbgtGjge1O3p=UAt5A zw2xQ!ec*7mW{eHL;k!KC{PdQ$Pd9wF+y1Zg%O!0~%Rf;`$@+d7C%(sC+&=rn_t>)E zw*m#GTl6Zny?7Vl{^w17Vu{JZm|0s3f1mrZWL54-^EWDDEq-B2OWucu3I4TDIk>pv zwfKa-@~vtgxYo3<+>x{Lc1mu>t>D*&VJs@EAGj|pnAy=U|JI1*e7VrVFVa2deme#q zl?_WwTJQcoyK9+kbRhG>ckyED)=y<(GM^I6ck#%Li!P4KSNR``Ns~UO_5ROX*}st= z&-dr?`noY(OVoLOc=Jt{CDKQZ$t*3um^SBz&8aT8b^#mb&u3zvrCwMX!^?H~ma@Qs zIP>G5Qha=y&7A%|+K_I$B>FF-_TM-|{yp#SblsH@{4mL-!fSra4MxpJ^Bx721g7)e zSu)E(A%@|nnF)_s_{;Qf|1+Go$O~qh$hX%XX^p&+oGA43h5MgZN0+Pp^yp@ljgw@U zExJm5$;?%|yxDKK&XhH@xTExidBx)sNB->*S+rVZN7O6vRUA&?K8{=U&I|aPG8#nJ z{aN&=?!aB6D^qG%ZauG9VAoe3!fqbhE4|OPrBfuFcljr=_3`Fe(ppbft0xO9ZC-SG zQcB~EuGJ_0@E!7crchzsqm^>A=3PeY(oH{vQ~xST-e11(&CYCo+vzc{3sicYm+mi> zRJiR&OBk7E9R{)^(fIr_T`q3 z2V<7ay6-QZf8)>c_-#BfyncD+tTPP?UPMQA=jHjCK5vh9VM*vnPJCf-%F%9(Z0gNw zvlmMa_c{9KcXS+)-#@*1LwahDa@nb`?EBZuRxUqtRrXnW0P|djig$%UD`sfCwb`Wi z`=(M1<9=4-3DfT^(U<(3!{)(QsbXo8c|){+na%$?uN9qh%TKMj#5TUp%pL%SP#+-AaG1YgxrO zXAE?v}rsc*wMA>N4VUz&X?@^(rVah5l8?0d0b zU*-MdADrdx*jgR4tdEuPbmhqo^tfykKL*592^f+-kHwv*)h?XGrY>k;*03IKX3AmeVabd=wL_P_Pz(Ex<_*E=}I++ z^}gk32|MuKF2#1AeEac*pKl~qEVSO*;rTmE5W4`?1t!c55)=itSpZm?Fbw0NqFY^=#S#Db25bu4;DOPdgb0O8gAu5av3fppK z#w;;k+_NaKX>0GD9aEXN-;fmAd*#L!8IS*k9Y%FAbI+cYzM61yny}c~`XtvQ)hC!0 z{_x}*^gRAI$1f#3tyCn4``X`mme0(qBLQt8X%HTbI@8-018?QqU6<5;3i<2mm~`U>xDCweM4e_|HT>AWK~ zZKG&%-6KAMwRc~$aG5c`3b=i8svv(O>-NYPrUxe{+1D)BUC>(lxpV#6SnoNBVGF+7 zTX}!zjyKK}Nep)>zF;UUdWuyb^I4udU&Z!4)(3Z)|D3%4|LtwZk6&bU33rUuKCBGN z8;PQuHy2MZ|M55a|C}d|?6>|K{}ADA5&NXdKKJ^`|E^mf%OpGAEBaD%Uq{Mj&Ohx5 zmal)vKRHtPHZ|etkpP?L8@GP`wvjpblH`GB;^wcCGg|l0$cbisSH?PJqDtbVf650H z7tB-FJ9Oj=ujt2&RKE2tu4*Eg#++Na@8lkj;LpA6;<7#d!;Lc{e=0M#-B9?E6v<*` zdd=Xs=hXU)z0=>$dvHqW(b`9U0?Q1VzRXWuSlY04c5BzMmH%94pZ$#Y;=EZJ*_vo%F)RSaaFq)-J`#KA+qp4Y$lp^h;Y-*x>oOC-cD!o%n6m zyIpE|ez>?LE3%~*{yD!}j(_ch=7)u`^;=?M{)#=5Yg)W!tA>T&+#=b-rEPw~k-Q$u z8o8&;HCeP!u6B*sisI8uA6Oed?pA2HJ3D6r(~lq7Uy?#o7whi%vwEHMF+26;-#b{= zAH5?cyVktKclEymYHW*B>T*=%_;O6@?-dz7>`VTbXWazrbs92hz*_x7xhVEnd9%lzN%ao9SCYhcbrL_FZvbI9EGu>)G7*C+}Ua z)X_h-?XTJz%c)xoR3#m9dhaZl6}Iu!vM1)ZgSM~lZG9*$od2M)Ve!0k9Vd@CA1n8l zKCAePK~*O?p;Y=^WBF|!=9Sm{o>j1B``&lX^_|s3>BwENxZ0+lD-d8;=nOD)V~ zS+i4QLuBC`_JAxI(f8@|ZeM!1v87s0+)SR){pF>XL9&fsZ5VsrZF+MwFN9m=&vh-Q z=Mx0pAG|heFMnC|@5OUh=B{Q{Uh#I*V#iYtA4pu#ubFpU-gL%ngYpZdH{9Eg?TEFp zxwG+Cs@6uYuBSq;HoyOR{6EiwIc*M8G+7>Af3WV^&91w5PE}4bDzFiB-;ni~d50T= zpKy`G%#?$tjEe+iV;9SszMeW?C;U1~a=NgAYu;mdt>3?vPBgPCvHirHb>D}_d-J~O zFExE_KUB_0m~ee-gQ@bj)6B{je8plCqt;0Jgn8wpXXb~WT|80T$*zk*AeYN?smZO! z6aO$D-tewQc+sUtO4*sq9a~B^Ij-$J8#ycBvxfOAll^v_EEZ=auI=0PBg`Z5&Bet! zi;gh!`_^^ksD)Y|)>ip*o&DXhz{6LSqQ0wdPyXM1o?}vJ#cox0ZYTG9316ox+kc<xH6+*Tn_Px6az|skm9+E#G%U0ojA?Dq@wr3`G?s~tdE-&!F4!4#>NzQ^_-{+-W@n~*Zyz}QOHZHHl zl9RjtJJr;>I>nw%;;Ovw5y6+VtGm7}@Z#+A3$0%Cm)<=xq55Z{pek43%L2D^D(0`G zoOqqSs(5JeAAa2FbLFXi?)9^A@}FJw7r1=LoiWF9#oXrpN5vivOYFXPy;AhZsC;l@ zbL_*-jY2_^ZpWTw>8jy|`aj;`Q@wm8ZTav8{P|HlpRxZQCU&HMcIFjPMu#n>ed_&Wg*G zGVNba>EG#eU%8e2^%|`on!HOs=4rjU#NMHI7~p&$6_KE-dXWg;nR%M7o=Am-+Z^mQ}BxIi5Du;3FWns_hlb0 zcspZBATvY3seY#&vntZhw(n^UmYw%1=lHZb{y#Cop`Tnd6t2d*SvTEsYi_80*!%bz z$GM0Uv;P6DJ?rnTuk)0?x;wS2E5CT_g*kAIiR*V)Q{uKm>g!sy0uJMFAjp?E^8_pZO^ zz4hlP>#M)8>t;?}8*IEe^PRoUZM~Bo501X$zRM$d=I0NlO`^KH5vPH?4%3F{3 zPvhCV`Ot4cP0`!tTTaF(-}}Aw$obTGy)Emj_uQ&-Fi6PVArX1vyqxs@El-}zo)TA4 zSgzE1C}jGCqcxKbyi%QZJjGar)#Jp(F1zFhUT-e@JY6txGV4j37e|?HEB(3l!~LmH zp_lNX9V$Yz)plxe6~9lbxb?*%`~2HQ%B9~QZ}#`OtQD|KrnJw9L&#?1`OZy3Gdr%| zI`8zkPWZ-O^97$*rB@w1yCMFM$)O+f-mYLN)~R2|s-E#>Q~CLjCv#7nl;I3pa!~Ec zW=odZh{Km{(oS1{xHO~VQT3Y@MjJF&?=f06XGSyEu2cK=pSt?l;@o)^SJ{1!?Goc5U-V|NjjF zyOtGQwmhD*cH^|9*PJyhSL!quy>_kcYM8UQ~;7e_XfsUeABk&q{x;u4H~P;m8S%bagS7M0=(dvy05yG8NT^xp`NPZ?SqN zf3fR`>x~<0<8od+teRt^w8d{V$AO;Z&-bQZ_`J`+_&A4nN{wIQlIr98Y@Jiy&6Mr@ zdtq*V%Kf=>=07yQZT|T8v_|_`)6-QJF3%Bgsaw3k{g{3Bj5q_26c43cUJ3Sx-Y?PK z>>gmmuKwoH?}*-Nrn$>$vdf57HNtReyLg{mbgNNKC7o|>en~|U(F~6&)h8jGIAdzY zv5D&h-S{Ymys zzl&`C`G-w6GjRD$Iabz<%dTW9 zuxjb<+kJ8WhXtLW@UWb4-sI^7=i(1PcXQ|LNlUK~+E*O!%B;JuKW&rs^dmvsPm<4w zyS$pvrV#qEyu|8Be&MR^{7VWyGQSRw{{H{D$(=14JtuS=AMf%|e0QT@@$Xjt*D)uq zpYi>AOYF7Y!%HXpYaXv{sdaRkc6joHgt}z@?nkZ{+$WS&&Sw0y!`)iH$wWHWz1u>% zrDmz;7ImYQhvdyfQZ~;1Wh*ADxUS&zG|_Vl>i2Hja`pPIsyxQJ1Jzyc(w)0Adh5@e zFKVa7ryX0(+)@{F8F3)4X(L|2;fywggrqGayAQ@rZ9qOqyxs;yEOATtEbo%?NU}7;p6Tt+ux%ZRJ&(m*-+hjHJ7dvF=5>cu*mnIE zKlNPpPqbX~rRze>UNtwL9M~R^X`3fE!M6H&L-N&>)#eEOz@e_CV8^Y_j++r4s=6gQ*>rPn^cYZ!1%;8lXxy{x9$ zDu1s1DBnLhkV$3rYpZ({qNM!1})_$(Mw~eDm)41c^jGevfF3% z`T0%7H*c*;V{eDtv>E-^6;AkSM?PSfHDTX3+dW@7r@8T&e4V-DWAB}u z%jE~dOiy>q3V#-LdEWd(B%uE$TnL) zmau3U-PRrZ%+@zY5^aPW7nZazLgbqi>d^J=yaL2?3&uP^*7PJOtKgZG$V zn{@@x(&oi$boah?k~2&H=d&)7VcPWUXDKbyubtS?w8V7zB)!es4wkQ+ka4zqMu4zm zNWfihyKcdQ>n;92mHcQjrG3Ha`&Lyin{Uao_-5Sr;Ip)9d+0aqkhS%%Dyr&hZY|em zxgZuhS=d@)LG^d*>VQY<&97fsyGOU+w|aHrw*_qNl1*2(y9Zq;uNDedIAvct)A+S2QpQ?LK@ z?w_^e@B9Ok`?PD=jjq;j{rdlw`(j=`3#)CNsce;d%LB?eR94?VbAhKbBTuGLCuBnA zTI)?uK2437yM1niJV#+#qr_4Dl@9{SjTZ%lEDwCFr?PA7)8?tnpbV(q|0nCWM2$zs z#jRqye3BiuZkl-Rh_B=o^QjFiy=5vrXYTept#No?Pt)1+d>d)qw6cT<19s}RhS zG>>ZwX*o4xlRqxvV#ezjniPST0byROgh>$sFu z?|6EKXU%1onuF~P`mvYZ`fge7`=@lht@N%N|CVr;nbl059~M=8y09VgWNSrRg6e!V z^QmhiS~AY)oqB%z(}d$Cft}r2S5|(rJ;&zu?5shFw$wZ81qE*%K1+tIGe2peprPry zQYz=mt;>6o*9Ja)@|B5w_wxLv&BESSI_JxAx-;xno<3(GYt@yf%PU`bY~4Edn#i7# z-0eaoFN})5l{y$Yw}TdT{&RVEZ2yi+jMv^@`|(lnS-VR~F=yKZbJnCJ@wYcLpB>e- ziePnWZ#cGm-UD^>C1W|s#$@lGbnwMnHTEllnSNU#O-tt3-HW>@NzPsZ9LrBI9hlTU_^aR)Z-G4Ie zMorw(t8U)}LeHjEG}Xw<9*eT&IJY7)k++L;&D9cx87l-9w*K6xA$+*f#LWKhdfSpM zDjPR$devcac(Y8rN7cTyE9@6|tNgjn%oF}p)M(jj$Ck7FvwM>d9xnXx?8UFAJTnjT z7MPof%rtF3w{d3wN5`=9b1j=To+<2TFODwLd-|k=Gt(l8_lz39GS34h=~??f#8+3( zwv^mZDX}YA_>aS(OO^91&}CpjD=Dj9GGuX zzIejoD*;9!m#x@2XT*hQ&gGXqaCPCe>P73mFL#@%)=_PL>uJh0zMvfcHg)$t+4WnG z3ktirKeH^GD$I3BRK#$S&-FQ98y~uDyZqHd{Rv<1qK4mpa~J+BR59hLUA=mC#sBc? zgzJk{{6QBUvUKD#F@OMQ=OHTt1B1@KZ5I^(3;h@T_n+f`=>J>)bN;XTf8+oD|1tlU z{tx|s>3{M675|U^x%N}N@LovrH|aUSJL<9x#h9L~G&0>)nehDRq#390ZE;+G>PK9( zs>AQomI7;PL}ukqk<1j9fAQec=HBz?dVE|zswP=Zyc{}Pr9{tGY2}W;YdCcGcWsit zeD_?*tPiUDayiRPH2SYCxHfymlVr1vkBi&ZaV=_`xp0CM-$CA-s+jACYXY2Or42VT z3g;@En6vdzLelbvOhLEUl=IftF0A=CS=H^#*KU@eS!E+Lg)b1a%V@&70?5@+7EhZ=J_b5*}rSPVVD*LkV60_`gX0KKL+&Z52Yk|bZXxV@^ ztFx}Vm>lJ^yU&=V%<{e*`Sb1TuB?#a)r-J#iH`BB05&1CP@9dB>nS6t0eGRMqa zLb>t@Q*nlix4zG(;>%V4T#wgs9j{L`nf_#U;3WHHk5o3zkuR1iZTjVUdb!KDRjdO4 z)H|Qd_|9_T>FqBfXBQ?O(zjDo4|XwsVj#(2lKrG6PwY{-p20uiIK$IQCm%?2Z4^4y z|7UaCoMLyrInxpoN*Cnu@_#!1_4j?_|1W=Wv9wjNom(=scVs`u^ow(%C&s3TIPZ+87owyBxl1a^1PJ)iu!Gxo5ZA28qRbS>o24 zy%xsSXiVSeVHo;mp~|1@=eVZava-*t(mpC3)xYAf=arw6SFAP5GEez8HAl*ceaU4O zF7DpqtsFaNWv-DbIJ;yI?Es;Wy5JGEjB^_B0gonXGE z;Y&>9?DRdGZc8ducpTi$cOzpagP@X}?XAh(Z&w`svDbC~q04Urjs_Yti^P24TVk;HcJGe`U9S{I9Mq2o(@I%elj6L0R52-aMjeNSWV_Qtot9JBatS1ro4 zl)1(8=+67+ZPQ+-x$omUVdbegU*V&U^&TV6FY<=QPcC(RKOOqElR;>=^3OXrUcNGV zQ+IgvPyUX*Kc7uas?Iqiz;e6&_$ie?vYR>n?R)U6u1B_7H#e{9xSLB#`rcVTK1EsF z*`Pl6T7a?AlfAzenF9At&Xghv;8uE3&Z;(f#&iW>GvGhru0ND`c`~K@^h1I7hm7& zOPb&F^b2Gz{Z>-(5X^KwR$KI~Z3`al;(pWrP=(#~iqXbB3NEK^B|YmpuFP`2`BvY|{MmP6 zTkAM}6ejY%(~#Ww)7T-=B~fqF4dd0ZrVidpmnN3T9?>=kSyvv@UAd^>yQP;LyK6ZU z>%(H#i*AXh9#_3Ql@z~~F+?g(;+WAIv#6Eo#%CTpW)QIsW_xydcda|~D;H_=UWv%Z zH+g4X`JuM6<&0w5x~JhSJF>4YV?FK^7V|FNz%xo?PUe=$T}S4B->~yOd$wTaMmRc3exLa{dK383B9~w{k3dmVbFFZ_NYgquKQ*;iQQhbVS1GOtbcvF z)6Sdh^3=RtV*U1e;veWdt5BPFEb>g3UPa~w2Pi)L`t(NanzGS!e zaL-k1wmH8Z@XO!ZI(MF)pr^V@b#|OE<1w+55~)%#8Vl@mIhI5vFa~;@{j%)aHnyYL z5A!$r3+CT{^>A^XhpI!e_StI7rkjlu!o5E6&py&&Q}xMr(U)yy;g`KKuBd$!uBehy ze=BcvbuaU}qwFu5&s|8|&9LwL@~>Z0-|k?2oNS+RS1h+*-*-}3=GRs0+*>Eyr6A}GnNX@z^7T=BAYYRb{=@^ zu-JCvJTt3nA1gHXpOd^&zs70Cwwx4BO-uFjYdybg|D|NBbGqec^INOc;-%fLzwULY ziLDcBoSbp8(ri<#!oG)gc4t%HPFr-=Hb3iwlSaVq@OK-UzAs*XBlqgY+Yfh~%PsKf z)OsA5dQW#v$T$Cbz3*P@_}(mEyZrBVqto}qTesDC=!kxqv-y_+S06)B)WMp$GlPQy z4_|vO_W1m~X-jegzfDMb_q+b&sibA;!P0BqrW#DN=-_xdeQI_O|1{4D%dWfqS)y0) z?|RV&GvoiiD=NQFc2#XO->x~WeM&J))UVCoife>tS5AE_mvfn;E9{}D?z>It?KXy& zVs3Bi;dS1)B(r6;@}Ck{=DvOATb)d&8R`3XuDs9ncVXTP~;$W@H>sPmNSyQ^eon0Vh^{5@h<{>S6T{)-)FQcQVx z=63h*XAb_Wj9Gs2Ifv9W$h<8z{Jr9IDa+~X7Pp0SEf#Z5-L(1BgNx=3y!K4-EC1Qe z$UAUQGgr)HeWZ17NRxoy!^ZTiXMY8byy+<{anh_j`$=P?Xr#MUendjWo)-t?FZ0IK_qT57b+0_ge`^1QMFnT1e#OptnScCY+QZ!zGiv0; zSs9`qq-;N5uzkx0F8{NEUuP`g-jpqLInYFoeVe<&fu|4L9G{6S({lKzFM9RPTa}n~ zZd+GyZn$OdQI@~DZTqU?oJ}5|_%f58SE_c|U#|Lda$0J|&45>>FSQ+n-=JNL!;aW6h-y-SX%pbSEn6=c~ME>!r z&v8qS=%3wuF5%6pNe(&F7BlaWWn%BXxnQMK)_RR&zoO6P3z@&Tp1;s|!qSs*lXLz! z#n-i{gfBZ`)|Id{ucYGsg?!_-H^Ma^`?FW=3%=0Fp?&X){ZW2BA;zxkd0X%A(G-fF zS+sWz-vi6^Jpq^dm-ZB#I=uVP3}>ltSGQI2?Z4wP`}LoPbC&H+R+xE%ZF*?1w@i$? zFq`?ynR7fBzT`Y<5wm~Sk13aYbSF3{zume2;r_P5#k&i>FVs)lcq30%^iih4)~w95 z#Q4h2)}L*uYaLeP`y_ZcG2T%)f9R+HgYU(++5Bu4zZYcO^mJ4{JGFX~OE%m6_YW@$ za~p)-NEhUMFT?Kef6n3LwHxcE*tpkbT@|a?GCNgb$Kmgn&U}{ESmE~J_>K#EdXkNK z-2*R`PtVg(EVusE#soUARjx%R_i5a7HEl8R35xmVyWsJhqkK;m_papf za-6%yoA>j&Ntbdb|2f6CJ-)b8t8CgYr^;4)e#Ui7$2}ulK4?jQ7Rn4tbx!D!|9bnL zNyzSnFR%A3J0SMphGy@tt>Vl68X8^>VoMF&Q`s}WcyiDKOU{y6cQ~ZZs z-HqTo8GG-8sA8Yz8{_@I3=Zm?-qh2ja z`tpkm8`usyuQc^GO}w+GlC5Z`(Shks49ToDW_gX9Zwp&&kvyp5aXIhf4}I6}TQhqE zwXSW56o`$Ntry#vwMRMs#P+Hu?8>(~{(je+o#5e|R43cMv8~WO-AQmgYgOIu{Z*3M zfAV5Kn5##hsQzQ`)csEEmFL!*Rwwr+&#zD19y`O(SxRNgzPXeCoSc?&nI*ogr{IP! zGtU{D-;Go3^9ySpPS$d`%ImjG@z0O)3!5@@r6MYVZ#DBq9XI5i;Sj>Bc1ZVb{QJ8J zYdQlhH+t_a&+Y9y{@eJj$SU>hs7q@*zU=?iKmV*iM&!;$I~iZ zyFjGxyzOoG4X2Juio2fWDa)&8e^|MTOKD5ylK|zL=jM23rc7{CP^$QQ>etru9Q?oU zCh4-AloG z(QtLq(yI=fUI!hWUbjEcIcB0{t+UiUW-Xf}dv5yGyIZ)fT%4S@=y1>Fq~J!*@7=y@ zw=Yfow_nMQ+a+v4-R*ztwa%=Pb5BXL_<1z;rI&S(scO$;Q>g7-Doa6+46>SRhOY@@`^2Hu7!PTSZotbWuGtEbmZfPH9;+_Za#>4 z^7g^g3uRr!(NcX}LJG|kEB z!~AeG|FM=tZbIsGz=TjaH3 zo|DV#rr=lcn^ZqNndV?8dg#J=mdse?5o+5iHowh2?bN+dp`cm+)Uo~VI+pb^&YQ7~ zY1_LB7Www=>t9H?%TJV-iERF?G=KX1)<%;rg-ted4_@B#xB7jQQjv(C`%97am-6{a ztllbpzA!(~uw=1=YRs%~$Df~l*SxymlO;Rzsc%DFt&qU863L72SSSA}&gia;v0K%} zv_L%2qEpYd$MBtB_}AT&+&rFZMJ#{7JjLY21uM-bd#hO&+6BDWtKIi?#yR`{KdSyLhiu8ONjBeCa|;x^6ut>iuUNSI0^6b7xqWBeRasmG>QDU0vJKCo8dES$sBN zeP|u;bpIK}iAx{zU(NqBMM}1lTZMCHt2wU$jST+4cm<1AM_3z|4i4RT}Xr*onw?81xR2!68w0go)k)^*R{^@7FTlPNhEwhwU8p|W61@lEq z3chViYMH0~L^1cv`VC*}F8!&vv+t*IXU>Y4tRMHQ!uOvw{CIzT$zIhBv9BIBroV1x z_RU{v;F0`s(RP;K$9V$2d}jat=%>z;E#VI8$+|0e&g4DznVA2d{eLQp#iUJ571KQL zIjbjamGZDkotY-`wa<9h)yWsUQlz5yeBKiJ|IV2YZ|B{Ki~2uJRCT$q-ug)=1DqUG zBYxZb=C2W3>9sj&vEG`$Hg*b3|15VlXg;y%JF|C!J=2ShHYWZnYYnycigkbbDfdWe z=Hbc;7M<*j4fDL(&p9q>-4;FFF*7~k+ETtq%T=#MSGK%va5?w?x26z3hgGXlR>4+# zo4akZqShV0cKqf>ql9Cx6Po0^a<})j?cDk@yWz~0;J?c`ax@k%sxbb0jQQ7UuFt%G z#TFmA>~K-AhgIR7%S7Q=h0({87ocI8e?`%P^pwCx8KFK!R@u`uIEZSR#wP0tQGxyQL6rD z=BN4AXPVhLmX_vkX;_{0Q6NQc;laEQGt6brA6)mot1NYM%zA#eD{Q;Ad~R|&xXS`;!_m&Ki{+O zP4|}H4llFCb2jGAyk)@nU_tgO&Gy?rH>`295BgqB;ozAVG75e&)wcL@IDE&p2XXg6aHRzp{UCOt(Nz-%wEupQM-8V}XJz8=uD|E8dzOOS+xmx`8HqNyy_&#Oc zyU9N+A1*mIZC;-DSyqi$4bjuvBUKXe@3xr+?UX*2f6e1haYje+;Zt8XdFv`~xo20b z6dv_xlhx_##_7jmrp?w= zG_xkB>TTbG_50SU&E1?Ay-{yv#-ob#6b2VB4)ezL?@ofue>IszjYJjQTyC?K94ljR zIhA?9K5#+6##`Syymvk~*njbd;=Q?b{bvi}YYvx3)qVLk?*QlGyoZoe%`D__-82_!7Z7TY@D+fC(9tL6XyQ;1&A=NkA*;h)-ujIEuk#g9+cNfJu@mggAo zz>ClNMxyOzy{4(R1#kVIY%JA(FqMn9`HZ~w+Sr992X`&8;L!-V!?(?F%gMDbd@qUI zQsF5!l|5&n%iNuQS>>njl?&ko?7>;@40O&XU;1tQ&)}5!`z>}_njRZGnSSTlSp2+_ zw(0cGb;2*rChSf7rk&T@8j!W-|E-iCg}bV(-GV;9&S!t@yI?JA&BbIU*Xt6xMMnEJ zYYWfdjH-O-UAR^Hj!xNr>-T~8imlj1k1+SJO-k=So5k*bh*A0v_cy+r@;~}vAQHkB z=;O;NncC=DV8rTbGHHYCIyQ$%8)Vk8IUKrK;rA56oV@*A;XaE>QPJo>sa@3{!lm-K zKc((xEu0^F$!yVtYcK!qswrB0c2)-C!*AEqR#n;@_WUz(qL7r-67&BicNb~${$KEQ zdH&tWm+#H{{QKYjS}EmgS+)MF)w7owK9<#3_4#i7>tE&dac5`mmY4Iou&?v6GMIE&rIb)SE88H)H;=wQ)hWPwf-Wnd0szYT8fKv>&<3`$J1i zi+9IO@8f6N=M?k_Ni;mWz9qs~ecK%wL+^Pv(^78lYZ5%Gf4F1TVH4e3%PthQ@9@n? z_#nw~x%1nBh|lXp=GK|r{U~tTZfe54IBABz-cyb$HyoTee@U?j2aCbBKj; z)p5(m&wYh<1o25^cPLC%owUZT-sk2Ti51>c{LM5!-dLS)cH>Z4-G+MGnp-U?s;}yg z%B4K*60j0v<~CezGaLDA-hI;HKcTS`wpx;yPq$ZMac6K=Y{E{!lyoikf6 z`_@|Nv-d7s=~H^RP}oXosowjkWmEWe2Mf-Mkh+llOx*T`#oDyvM>idw8=m18b-499 z4uQjUM7_^#uFIo3qvS0{hUuKH+Gd9G&l4vwdOdqTNw^IFUeK+~ literal 0 HcmV?d00001