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