2 Commits

Author SHA1 Message Date
Storm Dragon 447e2cc896 Missed one commented setting. 2026-06-18 10:51:44 -04:00
Storm Dragon 2b7d205f06 Fixed some half-completed work in generic drivers. Minor -x bug fixes. 2026-06-18 10:35:48 -04:00
13 changed files with 426 additions and 120 deletions
+15 -15
View File
@@ -19,14 +19,14 @@ volume=0.7
# shell commands for generic sound driver # shell commands for generic sound driver
# the folowing variable are substituted # the folowing variable are substituted
# fenrirVolume = the current volume setting # fenrir_volume = the current volume setting
# fenrirSoundFile = the soundfile for an soundicon # fenrir_sound_file = the soundfile for an soundicon
# fenrirFrequence = the frequency to play # fenrir_frequency = the frequency to play
# fenrirDuration = the duration of the frequency # fenrir_duration = the duration of the frequency
# the following command is used to play a soundfile # the following command is used to play a soundfile
generic_play_file_command=play -q -v fenrirVolume fenrirSoundFile generic_play_file_command=play -q -v fenrir_volume fenrir_sound_file
#the following command is used to generate a frequency beep #the following command is used to generate a frequency beep
generic_frequency_command=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence generic_frequency_command=play -q -v fenrir_volume -n -c1 synth fenrir_duration sine fenrir_frequency
# Enable progress bar monitoring with ascending tones by default # Enable progress bar monitoring with ascending tones by default
progress_monitoring=True progress_monitoring=True
@@ -114,17 +114,17 @@ max_batch_lines=100
# Only enable flood control if this many new lines appear in the window # Only enable flood control if this many new lines appear in the window
flood_line_threshold=500 flood_line_threshold=500
# genericSpeechCommand is the command that is executed for talking # generic_speech_command is the command that is executed for talking
# the following variables are replaced with values # the following variables are replaced with values
# fenrirText = is the text that should be spoken # fenrir_text = is the text that should be spoken
# fenrirModule = may be the speech module like used in speech-dispatcher, not every TTY need this # fenrir_module = may be the speech module like used in speech-dispatcher, not every TTY need this
# fenrirLanguage = the language # fenrir_language = the language
# fenrirVoice = is the current voice that should be used. Set the voice variable above. # fenrir_voice = is the current voice that should be used. Set the voice variable above.
# the current volume, pitch and rate is calculated like this # the current volume, pitch and rate is calculated like this
# value = min + settingValue * (min - max ) # value = min + settingValue * (min - max )
# fenrirVolume = is replaced with the current volume # fenrir_volume = is replaced with the current volume
# fenrirPitch = is replaced with the current pitch # fenrir_pitch = is replaced with the current pitch
# fenrirRate = is replaced with the current speed (speech rate) # fenrir_rate = is replaced with the current speed (speech rate)
generic_speech_command=espeak-ng -a fenrir_volume -s fenrir_rate -p fenrir_pitch -v fenrir_voice -- "fenrir_text" generic_speech_command=espeak-ng -a fenrir_volume -s fenrir_rate -p fenrir_pitch -v fenrir_voice -- "fenrir_text"
# min and max values of the TTS system that is used in generic_speech_command # min and max values of the TTS system that is used in generic_speech_command
@@ -201,7 +201,7 @@ new_line_pause=True
number_of_clipboards=50 number_of_clipboards=50
# used path for "export_clipboard_to_file" # used path for "export_clipboard_to_file"
# $user is replaced by username # $user is replaced by username
#clipboardExportPath=/home/$user/fenrirClipboard #clipboard_export_path=/home/$user/fenrirClipboard
clipboard_export_path=/tmp/fenrirClipboard clipboard_export_path=/tmp/fenrirClipboard
# Convert text emoticons like :) to descriptive text (e.g., "smiling face") # Convert text emoticons like :) to descriptive text (e.g., "smiling face")
emoticons=True emoticons=True
+12 -12
View File
@@ -1506,38 +1506,38 @@ Values: `+0.0+` is quietest, `+1.0+` is loudest.
The generic sound driver uses shell commands for play sound and The generic sound driver uses shell commands for play sound and
frequencies. frequencies.
`+genericPlayFileCommand+` defines the command that is used to play a `+generic_play_file_command+` defines the command that is used to play a
sound file. sound file.
.... ....
generic_play_file_command=<your command for playing a file> generic_play_file_command=<your command for playing a file>
.... ....
`+genericFrequencyCommand+` defines the command that is used playing `+generic_frequency_command+` defines the command that is used playing
frequencies. frequencies.
.... ....
generic_frequency_command=<your command for playing a frequence> generic_frequency_command=<your command for playing a frequence>
.... ....
The following variables are substituted in `+genericPlayFileCommand+` The following variables are substituted in `+generic_play_file_command+`
and `+genericFrequencyCommand+`: and `+generic_frequency_command+`:
* `+fenrirVolume+` = the current volume setting * `+fenrir_volume+` = the current volume setting
* `+fenrirSoundFile+` = the sound file for an sound icon * `+fenrir_sound_file+` = the sound file for an sound icon
* `+fenrirFrequence+` = the frequency to play * `+fenrir_frequency+` = the frequency to play
* `+fenrirDuration+` = the duration of the frequency * `+fenrir_duration+` = the duration of the frequency
Example genericPlayFileCommand (default) Example generic_play_file_command (default)
.... ....
generic_play_file_command=play -q -v fenrirVolume fenrirSoundFile generic_play_file_command=play -q -v fenrir_volume fenrir_sound_file
.... ....
Example genericFrequencyCommand (default) Example generic_frequency_command (default)
.... ....
generic_frequency_command=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence generic_frequency_command=play -q -v fenrir_volume -n -c1 synth fenrir_duration sine fenrir_frequency
.... ....
==== Speech ==== Speech
+24 -24
View File
@@ -857,21 +857,21 @@ Values: ''0.0'' is quietest, ''1.0'' is loudest.
=== Generic Driver === === Generic Driver ===
The generic sound driver uses shell commands for play sound and frequencies. The generic sound driver uses shell commands for play sound and frequencies.
''genericPlayFileCommand'' defines the command that is used to play a sound file. ''generic_play_file_command'' defines the command that is used to play a sound file.
generic_play_file_command=<your command for playing a file> generic_play_file_command=<your command for playing a file>
''genericFrequencyCommand'' defines the command that is used playing frequencies. ''generic_frequency_command'' defines the command that is used playing frequencies.
generic_frequency_command=<your command for playing a frequence> generic_frequency_command=<your command for playing a frequence>
The following variables are substituted in ''genericPlayFileCommand'' and ''genericFrequencyCommand'': The following variables are substituted in ''generic_play_file_command'' and ''generic_frequency_command'':
* ''fenrirVolume'' = the current volume setting * ''fenrir_volume'' = the current volume setting
* ''fenrirSoundFile'' = the sound file for an sound icon * ''fenrir_sound_file'' = the sound file for an sound icon
* ''fenrirFrequence'' = the frequency to play * ''fenrir_frequency'' = the frequency to play
* ''fenrirDuration'' = the duration of the frequency * ''fenrir_duration'' = the duration of the frequency
Example genericPlayFileCommand (default) Example generic_play_file_command (default)
generic_play_file_command=play -q -v fenrirVolume fenrirSoundFile generic_play_file_command=play -q -v fenrir_volume fenrir_sound_file
Example genericFrequencyCommand (default) Example generic_frequency_command (default)
generic_frequency_command=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence generic_frequency_command=play -q -v fenrir_volume -n -c1 synth fenrir_duration sine fenrir_frequency
==== Speech ==== ==== Speech ====
Speech is configured in section ''[speech]''. Speech is configured in section ''[speech]''.
Turn speech on or off: Turn speech on or off:
@@ -945,22 +945,22 @@ Values: on=''True'', off=''False''
=== Generic Driver === === Generic Driver ===
The generic speech driver uses shell commands for speech synthisus. The generic speech driver uses shell commands for speech synthisus.
''genericSpeechCommand'' defines the command that is executed for creating speech ''generic_speech_command'' defines the command that is executed for creating speech
The following variables are substituted in ''genericSpeechCommand'': The following variables are substituted in ''generic_speech_command'':
* ''FenrirText'' = is the text that should be spoken * ''fenrir_text'' = is the text that should be spoken
* ''fenrirModule'' = may be the speech module like used in speech-dispatcher, not every TTY needs this * ''fenrir_module'' = may be the speech module like used in speech-dispatcher, not every TTY needs this
* ''fenrirLanguage'' = the language to speak in * ''fenrir_language'' = the language to speak in
* ''fenrirVoice'' = is the current voice that should be used * ''fenrir_voice'' = is the current voice that should be used
* ''fenrirVolume'' = is replaced with the current volume * ''fenrir_volume'' = is replaced with the current volume
* ''fenrirPitch'' = is replaced with the current pitch * ''fenrir_pitch'' = is replaced with the current pitch
* ''fenrirRate'' = is replaced with the current speed (speech rate) * ''fenrir_rate'' = is replaced with the current speed (speech rate)
Example genericSpeechCommand (default): Example generic_speech_command (default):
generic_speech_command=espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice "fenrirText" generic_speech_command=espeak -a fenrir_volume -s fenrir_rate -p fenrir_pitch -v fenrir_voice "fenrir_text"
These are the minimum and maximum values of the TTS system used in genericSpeechCommand. They are needed to calculate the abstract range in volume, rate and pitch 0.0 - 1.0. These are the minimum and maximum values of the TTS system used in generic_speech_command. They are needed to calculate the abstract range in volume, rate and pitch 0.0 - 1.0.
FenrirMinVolume=0 fenrir_min_volume=0
fenrir_max_volume=200 fenrir_max_volume=200
fenrir_min_pitch=0 fenrir_min_pitch=0
fenrir_max_pitch=99 fenrir_max_pitch=99
@@ -22,4 +22,7 @@ command_info = {
# 'curr_command': '', # 'curr_command': '',
"lastCommandExecutionTime": time.time(), "lastCommandExecutionTime": time.time(),
"lastCommandRequestTime": time.time(), "lastCommandRequestTime": time.time(),
"lastCommand": "",
"lastCommandSection": "",
"lastCommandRunTime": 0,
} }
@@ -474,6 +474,10 @@ class CommandManager:
def run_command(self, command, section="commands"): def run_command(self, command, section="commands"):
if self.command_exists(command, section): if self.command_exists(command, section):
try: try:
command_time = time.time()
self.env["commandInfo"]["lastCommand"] = command
self.env["commandInfo"]["lastCommandSection"] = section
self.env["commandInfo"]["lastCommandRunTime"] = command_time
self.env["runtime"]["DebugManager"].write_debug_out( self.env["runtime"]["DebugManager"].write_debug_out(
"run_command command:" + section + "." + command, "run_command command:" + section + "." + command,
debug.DebugLevel.INFO, debug.DebugLevel.INFO,
+11 -3
View File
@@ -12,8 +12,13 @@ settings_data = {
"driver": "genericDriver", "driver": "genericDriver",
"theme": "default", "theme": "default",
"volume": 1.0, "volume": 1.0,
"generic_play_file_command": "play -q -v fenrirVolume fenrirSoundFile", "generic_play_file_command": (
"generic_frequency_command": "play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence", "play -q -v fenrir_volume fenrir_sound_file"
),
"generic_frequency_command": (
"play -q -v fenrir_volume -n -c1 synth "
"fenrir_duration sine fenrir_frequency"
),
"progress_monitoring": True, "progress_monitoring": True,
}, },
"speech": { "speech": {
@@ -38,7 +43,10 @@ settings_data = {
"batch_flush_interval": 0.5, "batch_flush_interval": 0.5,
"max_batch_lines": 100, "max_batch_lines": 100,
"flood_line_threshold": 500, "flood_line_threshold": 500,
"generic_speech_command": 'espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice "fenrirText"', "generic_speech_command": (
"espeak -a fenrir_volume -s fenrir_rate -p fenrir_pitch "
'-v fenrir_voice "fenrir_text"'
),
"fenrir_min_volume": 0, "fenrir_min_volume": 0,
"fenrir_max_volume": 200, "fenrir_max_volume": 200,
"fenrir_min_pitch": 0, "fenrir_min_pitch": 0,
+2 -2
View File
@@ -4,5 +4,5 @@
# Fenrir TTY screen reader # Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors. # By Chrys, Storm Dragon, and contributors.
version = "2026.06.01" version = "2026.06.18"
code_name = "master" code_name = "testing"
@@ -304,6 +304,11 @@ class driver(screenDriver):
"keyboard", "interrupt_on_key_press_filter" "keyboard", "interrupt_on_key_press_filter"
).strip(): ).strip():
return return
self.env["runtime"]["DebugManager"].write_debug_out(
"ptyDriver interrupt_output_on_stdin_input: "
+ repr(msg_bytes),
debug.DebugLevel.INFO,
)
self.start_stdin_interrupt_thread() self.start_stdin_interrupt_thread()
def start_stdin_interrupt_thread(self): def start_stdin_interrupt_thread(self):
@@ -335,10 +340,43 @@ class driver(screenDriver):
return return
if self.handle_vmenu_stdin_input(msg_bytes, event_queue): if self.handle_vmenu_stdin_input(msg_bytes, event_queue):
return return
if self.suppress_recent_review_stdin_tail(msg_bytes):
return
self.record_stdin_keypress(msg_bytes) self.record_stdin_keypress(msg_bytes)
self.interrupt_output_on_stdin_input(msg_bytes) self.interrupt_output_on_stdin_input(msg_bytes)
self.inject_text_to_screen(msg_bytes) self.inject_text_to_screen(msg_bytes)
def suppress_recent_review_stdin_tail(self, msg_bytes):
if not self.is_keyboard_control_sequence(msg_bytes):
return False
try:
command_info = self.env["commandInfo"]
last_command = command_info.get("lastCommand", "")
last_section = command_info.get("lastCommandSection", "")
last_run_time = command_info.get("lastCommandRunTime", 0)
except Exception:
return False
if last_section != "commands" or not last_command.startswith("REVIEW_"):
return False
if time.time() - last_run_time > 0.8:
return False
self.env["runtime"]["DebugManager"].write_debug_out(
"ptyDriver suppressing recent review stdin tail: "
+ repr(msg_bytes),
debug.DebugLevel.INFO,
)
return True
def is_keyboard_control_sequence(self, msg_bytes):
if not msg_bytes:
return False
if msg_bytes.startswith(b"\x1b"):
return True
if len(msg_bytes) == 1:
value = msg_bytes[0]
return value < 32 and msg_bytes not in [b"\t", b"\n", b"\r"]
return False
def handle_vmenu_stdin_input(self, msg_bytes, event_queue): def handle_vmenu_stdin_input(self, msg_bytes, event_queue):
if not self.is_vmenu_active(): if not self.is_vmenu_active():
return False return False
@@ -26,15 +26,15 @@ class driver(sound_driver):
Attributes: Attributes:
proc: Currently running subprocess for sound playback proc: Currently running subprocess for sound playback
soundFileCommand (str): Command template for playing sound files sound_file_command (str): Command template for playing sound files
frequenceCommand (str): Command template for generating frequencies frequency_command (str): Command template for generating frequencies
""" """
def __init__(self): def __init__(self):
sound_driver.__init__(self) sound_driver.__init__(self)
self.proc = None self.proc = None
self.soundType = "" self.sound_type = ""
self.soundFileCommand = "" self.sound_file_command = ""
self.frequenceCommand = "" self.frequency_command = ""
def initialize(self, environment): def initialize(self, environment):
"""Initialize the generic sound driver. """Initialize the generic sound driver.
@@ -46,17 +46,20 @@ class driver(sound_driver):
environment: Fenrir environment dictionary with settings environment: Fenrir environment dictionary with settings
""" """
self.env = environment self.env = environment
self.soundFileCommand = self.env["runtime"][ self.sound_file_command = self.env["runtime"][
"SettingsManager" "SettingsManager"
].get_setting("sound", "generic_play_file_command") ].get_setting("sound", "generic_play_file_command")
self.frequenceCommand = self.env["runtime"][ self.frequency_command = self.env["runtime"][
"SettingsManager" "SettingsManager"
].get_setting("sound", "generic_frequency_command") ].get_setting("sound", "generic_frequency_command")
if self.soundFileCommand == "": if self.sound_file_command == "":
self.soundFileCommand = "play -q -v fenrirVolume fenrirSoundFile" self.sound_file_command = (
if self.frequenceCommand == "": "play -q -v fenrir_volume fenrir_sound_file"
self.frequenceCommand = ( )
"play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence" if self.frequency_command == "":
self.frequency_command = (
"play -q -v fenrir_volume -n -c1 synth "
"fenrir_duration sine fenrir_frequency"
) )
self._initialized = True self._initialized = True
@@ -75,22 +78,22 @@ class driver(sound_driver):
return return
if interrupt: if interrupt:
self.cancel() self.cancel()
popen_frequence_command = shlex.split(self.frequenceCommand) popen_frequency_command = shlex.split(self.frequency_command)
for idx, word in enumerate(popen_frequence_command): for idx, word in enumerate(popen_frequency_command):
word = word.replace( word = word.replace(
"fenrirVolume", str(self.volume * adjust_volume) "fenrir_volume", str(self.volume * adjust_volume)
) )
word = word.replace("fenrirDuration", str(duration)) word = word.replace("fenrir_duration", str(duration))
word = word.replace("fenrirFrequence", str(frequence)) word = word.replace("fenrir_frequency", str(frequence))
popen_frequence_command[idx] = word popen_frequency_command[idx] = word
self.proc = subprocess.Popen( self.proc = subprocess.Popen(
popen_frequence_command, popen_frequency_command,
stdin=None, stdin=None,
stdout=None, stdout=None,
stderr=None, stderr=None,
shell=False, shell=False,
) )
self.soundType = "frequence" self.sound_type = "frequence"
def play_sound_file(self, file_path, interrupt=True): def play_sound_file(self, file_path, interrupt=True):
"""Play a sound file. """Play a sound file.
@@ -108,13 +111,13 @@ class driver(sound_driver):
if not os.path.isfile(file_path) or ".." in file_path: if not os.path.isfile(file_path) or ".." in file_path:
return return
popen_sound_file_command = shlex.split(self.soundFileCommand) popen_sound_file_command = shlex.split(self.sound_file_command)
for idx, word in enumerate(popen_sound_file_command): for idx, word in enumerate(popen_sound_file_command):
word = word.replace("fenrirVolume", str(self.volume)) word = word.replace("fenrir_volume", str(self.volume))
word = word.replace("fenrirSoundFile", shlex.quote(str(file_path))) word = word.replace("fenrir_sound_file", str(file_path))
popen_sound_file_command[idx] = word popen_sound_file_command[idx] = word
self.proc = subprocess.Popen(popen_sound_file_command, shell=False) self.proc = subprocess.Popen(popen_sound_file_command, shell=False)
self.soundType = "file" self.sound_type = "file"
def cancel(self): def cancel(self):
"""Cancel currently playing sound. """Cancel currently playing sound.
@@ -123,9 +126,9 @@ class driver(sound_driver):
""" """
if not self._initialized: if not self._initialized:
return return
if self.soundType == "": if self.sound_type == "":
return return
if self.soundType == "file": if self.sound_type == "file":
self.proc.kill() self.proc.kill()
try: try:
# Wait for process to finish to prevent zombies # Wait for process to finish to prevent zombies
@@ -134,7 +137,7 @@ class driver(sound_driver):
pass # Process already terminated pass # Process already terminated
except Exception as e: except Exception as e:
pass # Handle any other wait errors pass # Handle any other wait errors
if self.soundType == "frequence": if self.sound_type == "frequence":
self.proc.kill() self.proc.kill()
try: try:
# Wait for process to finish to prevent zombies # Wait for process to finish to prevent zombies
@@ -143,4 +146,4 @@ class driver(sound_driver):
pass # Process already terminated pass # Process already terminated
except Exception as e: except Exception as e:
pass # Handle any other wait errors pass # Handle any other wait errors
self.soundType = "" self.sound_type = ""
@@ -30,49 +30,52 @@ class driver(speech_driver):
def __init__(self): def __init__(self):
speech_driver.__init__(self) speech_driver.__init__(self)
self.proc = None self.proc = None
self.speechThread = Thread(target=self.worker) self.speech_thread = Thread(target=self.worker)
self.lock = Lock() self.lock = Lock()
self.textQueue = SpeakQueue() self.text_queue = SpeakQueue()
def initialize(self, environment): def initialize(self, environment):
self.env = environment self.env = environment
self.minVolume = self.env["runtime"][ self.min_volume = self.env["runtime"][
"SettingsManager" "SettingsManager"
].get_setting_as_int("speech", "fenrir_min_volume") ].get_setting_as_int("speech", "fenrir_min_volume")
self.maxVolume = self.env["runtime"][ self.max_volume = self.env["runtime"][
"SettingsManager" "SettingsManager"
].get_setting_as_int("speech", "fenrir_max_volume") ].get_setting_as_int("speech", "fenrir_max_volume")
self.minPitch = self.env["runtime"][ self.min_pitch = self.env["runtime"][
"SettingsManager" "SettingsManager"
].get_setting_as_int("speech", "fenrir_min_pitch") ].get_setting_as_int("speech", "fenrir_min_pitch")
self.maxPitch = self.env["runtime"][ self.max_pitch = self.env["runtime"][
"SettingsManager" "SettingsManager"
].get_setting_as_int("speech", "fenrir_max_pitch") ].get_setting_as_int("speech", "fenrir_max_pitch")
self.minRate = self.env["runtime"][ self.min_rate = self.env["runtime"][
"SettingsManager" "SettingsManager"
].get_setting_as_int("speech", "fenrir_min_rate") ].get_setting_as_int("speech", "fenrir_min_rate")
self.maxRate = self.env["runtime"][ self.max_rate = self.env["runtime"][
"SettingsManager" "SettingsManager"
].get_setting_as_int("speech", "fenrir_max_rate") ].get_setting_as_int("speech", "fenrir_max_rate")
self.speechCommand = self.env["runtime"][ self.speech_command = self.env["runtime"][
"SettingsManager" "SettingsManager"
].get_setting("speech", "generic_speech_command") ].get_setting("speech", "generic_speech_command")
if self.speechCommand == "": if self.speech_command == "":
self.speechCommand = 'espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice -- "fenrirText"' self.speech_command = (
"espeak -a fenrir_volume -s fenrir_rate -p fenrir_pitch "
'-v fenrir_voice -- "fenrir_text"'
)
if False: # for debugging overwrite here if False: # for debugging overwrite here
# self.speechCommand = 'spd-say --wait -r 100 -i 100 "fenrirText"' # self.speech_command = 'spd-say --wait -r 100 -i 100 "fenrir_text"'
self.speechCommand = 'flite -t "fenrirText"' self.speech_command = 'flite -t "fenrir_text"'
self._is_initialized = True self._is_initialized = True
if self._is_initialized: if self._is_initialized:
self.speechThread.start() self.speech_thread.start()
def shutdown(self): def shutdown(self):
if not self._is_initialized: if not self._is_initialized:
return return
self.cancel() self.cancel()
self.textQueue.put(-1) self.text_queue.put(-1)
def speak(self, text, queueable=True, ignore_punctuation=False): def speak(self, text, queueable=True, ignore_punctuation=False):
if not self._is_initialized: if not self._is_initialized:
@@ -88,7 +91,7 @@ class driver(speech_driver):
"language": self.language, "language": self.language,
"voice": self.voice, "voice": self.voice,
} }
self.textQueue.put(utterance.copy()) self.text_queue.put(utterance.copy())
def cancel(self): def cancel(self):
if not self._is_initialized: if not self._is_initialized:
@@ -129,7 +132,7 @@ class driver(speech_driver):
def clear_buffer(self): def clear_buffer(self):
if not self._is_initialized: if not self._is_initialized:
return return
self.textQueue.clear() self.text_queue.clear()
def set_voice(self, voice): def set_voice(self, voice):
if not self._is_initialized: if not self._is_initialized:
@@ -140,13 +143,13 @@ class driver(speech_driver):
if not self._is_initialized: if not self._is_initialized:
return return
self.pitch = str( self.pitch = str(
self.minPitch + pitch * (self.maxPitch - self.minPitch) self.min_pitch + pitch * (self.max_pitch - self.min_pitch)
) )
def set_rate(self, rate): def set_rate(self, rate):
if not self._is_initialized: if not self._is_initialized:
return return
self.rate = str(self.minRate + rate * (self.maxRate - self.minRate)) self.rate = str(self.min_rate + rate * (self.max_rate - self.min_rate))
def set_module(self, module): def set_module(self, module):
if not self._is_initialized: if not self._is_initialized:
@@ -162,12 +165,29 @@ class driver(speech_driver):
if not self._is_initialized: if not self._is_initialized:
return return
self.volume = str( self.volume = str(
self.minVolume + volume * (self.maxVolume - self.minVolume) self.min_volume + volume * (self.max_volume - self.min_volume)
) )
def _build_speech_command(self, utterance):
replacements = {
"fenrir_volume": str(utterance["volume"]),
"fenrir_module": str(utterance["module"]),
"fenrir_language": str(utterance["language"]),
"fenrir_voice": str(utterance["voice"]),
"fenrir_pitch": str(utterance["pitch"]),
"fenrir_rate": str(utterance["rate"]),
"fenrir_text": shlex.quote(str(utterance["text"])),
}
speech_command = shlex.split(self.speech_command)
for idx, word in enumerate(speech_command):
for placeholder, value in replacements.items():
word = word.replace(placeholder, value)
speech_command[idx] = word
return speech_command
def worker(self): def worker(self):
while True: while True:
utterance = self.textQueue.get() utterance = self.text_queue.get()
if isinstance(utterance, int): if isinstance(utterance, int):
if utterance == -1: if utterance == -1:
@@ -209,21 +229,7 @@ class driver(speech_driver):
if not isinstance(utterance["rate"], str): if not isinstance(utterance["rate"], str):
utterance["rate"] = "" utterance["rate"] = ""
popen_speech_command = shlex.split(self.speechCommand) popen_speech_command = self._build_speech_command(utterance)
for idx, word in enumerate(popen_speech_command):
word = word.replace("fenrirVolume", str(utterance["volume"]))
word = word.replace("fenrirModule", str(utterance["module"]))
word = word.replace(
"fenrirLanguage", str(utterance["language"])
)
word = word.replace("fenrirVoice", str(utterance["voice"]))
word = word.replace("fenrirPitch", str(utterance["pitch"]))
word = word.replace("fenrirRate", str(utterance["rate"]))
# Properly quote text to prevent command injection
word = word.replace(
"fenrirText", shlex.quote(str(utterance["text"]))
)
popen_speech_command[idx] = word
try: try:
self.env["runtime"]["DebugManager"].write_debug_out( self.env["runtime"]["DebugManager"].write_debug_out(
+114
View File
@@ -0,0 +1,114 @@
from configparser import ConfigParser
from pathlib import Path
from unittest.mock import Mock
import pytest
from fenrirscreenreader.core.settingsData import settings_data
from fenrirscreenreader.soundDriver import genericDriver
class SettingsManager:
def __init__(self, settings):
self.settings = settings
def get_setting(self, section, setting):
return self.settings[setting]
def _sound_driver(settings):
sound_driver = genericDriver.driver()
sound_driver.initialize(
{"runtime": {"SettingsManager": SettingsManager(settings)}}
)
sound_driver.set_volume(0.75)
return sound_driver
def _configured_sound_settings():
config = ConfigParser(interpolation=None)
config.read(Path(__file__).parents[2] / "config/settings/settings.conf")
return {
"generic_play_file_command": config.get(
"sound", "generic_play_file_command"
),
"generic_frequency_command": config.get(
"sound", "generic_frequency_command"
),
}
def test_play_frequency_replaces_snake_case_placeholders(monkeypatch):
popen = Mock()
monkeypatch.setattr(genericDriver.subprocess, "Popen", popen)
sound_driver = _sound_driver(
{
"generic_play_file_command": "player fenrir_sound_file",
"generic_frequency_command": (
"tone --volume fenrir_volume --duration fenrir_duration "
"--frequency fenrir_frequency"
),
}
)
sound_driver.play_frequence(
440, 0.25, adjust_volume=0.5, interrupt=False
)
assert popen.call_args.args[0] == [
"tone",
"--volume",
"0.375",
"--duration",
"0.25",
"--frequency",
"440",
]
def test_play_sound_file_replaces_snake_case_placeholders(
monkeypatch, tmp_path
):
popen = Mock()
monkeypatch.setattr(genericDriver.subprocess, "Popen", popen)
sound_file = tmp_path / "sound file.ogg"
sound_file.write_bytes(b"")
sound_driver = _sound_driver(
{
"generic_play_file_command": (
"player --volume fenrir_volume fenrir_sound_file"
),
"generic_frequency_command": "tone fenrir_frequency",
}
)
sound_driver.play_sound_file(str(sound_file), interrupt=False)
assert popen.call_args.args[0] == [
"player",
"--volume",
"0.75",
str(sound_file),
]
@pytest.mark.parametrize(
"settings",
[settings_data["sound"], _configured_sound_settings()],
)
def test_default_sound_commands_use_supported_placeholders(
monkeypatch, tmp_path, settings
):
popen = Mock()
monkeypatch.setattr(genericDriver.subprocess, "Popen", popen)
sound_file = tmp_path / "sound.ogg"
sound_file.write_bytes(b"")
sound_driver = _sound_driver(settings)
sound_driver.play_frequence(
440, 0.25, adjust_volume=0.5, interrupt=False
)
sound_driver.play_sound_file(str(sound_file), interrupt=False)
for call in popen.call_args_list:
assert not any("fenrir_" in argument for argument in call.args[0])
+73
View File
@@ -0,0 +1,73 @@
from configparser import ConfigParser
from pathlib import Path
import pytest
from fenrirscreenreader.core.settingsData import settings_data
from fenrirscreenreader.speechDriver import genericDriver
def test_build_speech_command_replaces_snake_case_placeholders():
speech_driver = genericDriver.driver()
speech_driver.speech_command = (
"synth --volume fenrir_volume --module fenrir_module "
"--language fenrir_language --voice fenrir_voice "
"--pitch fenrir_pitch --rate fenrir_rate fenrir_text"
)
utterance = {
"volume": "150",
"module": "module-name",
"language": "en-US",
"voice": "voice-name",
"pitch": "50",
"rate": "240",
"text": "hello",
}
assert speech_driver._build_speech_command(utterance) == [
"synth",
"--volume",
"150",
"--module",
"module-name",
"--language",
"en-US",
"--voice",
"voice-name",
"--pitch",
"50",
"--rate",
"240",
"hello",
]
def _configured_speech_command():
config = ConfigParser(interpolation=None)
config.read(Path(__file__).parents[2] / "config/settings/settings.conf")
return config.get("speech", "generic_speech_command")
@pytest.mark.parametrize(
"speech_command",
[
settings_data["speech"]["generic_speech_command"],
_configured_speech_command(),
],
)
def test_default_speech_commands_use_supported_placeholders(speech_command):
speech_driver = genericDriver.driver()
speech_driver.speech_command = speech_command
utterance = {
"volume": "150",
"module": "",
"language": "en-US",
"voice": "voice-name",
"pitch": "50",
"rate": "240",
"text": "hello",
}
built_command = speech_driver._build_speech_command(utterance)
assert not any("fenrir_" in argument for argument in built_command)
+57
View File
@@ -70,6 +70,7 @@ def test_pty_stdin_input_interrupts_output_when_all_keys_interrupt_enabled():
"runtime": { "runtime": {
"SettingsManager": settings_manager, "SettingsManager": settings_manager,
"OutputManager": output_manager, "OutputManager": output_manager,
"DebugManager": Mock(write_debug_out=Mock()),
} }
} }
@@ -254,6 +255,7 @@ def test_pty_stdin_input_honors_interrupt_disabled():
"runtime": { "runtime": {
"SettingsManager": settings_manager, "SettingsManager": settings_manager,
"OutputManager": output_manager, "OutputManager": output_manager,
"DebugManager": Mock(write_debug_out=Mock()),
} }
} }
@@ -273,6 +275,7 @@ def test_pty_stdin_input_leaves_filtered_interrupts_to_key_events():
"runtime": { "runtime": {
"SettingsManager": settings_manager, "SettingsManager": settings_manager,
"OutputManager": output_manager, "OutputManager": output_manager,
"DebugManager": Mock(write_debug_out=Mock()),
} }
} }
@@ -281,6 +284,60 @@ def test_pty_stdin_input_leaves_filtered_interrupts_to_key_events():
output_manager.interrupt_output.assert_not_called() output_manager.interrupt_output.assert_not_called()
@pytest.mark.unit
def test_pty_recent_review_escape_tail_is_consumed_without_interrupt_or_injection():
pty_driver = PtyDriver()
event_queue = Mock()
settings_manager = Mock()
settings_manager.get_setting_as_bool.return_value = True
settings_manager.get_setting.return_value = ""
output_manager = Mock()
pty_driver.env = {
"commandInfo": {
"lastCommand": "REVIEW_PREV_LINE",
"lastCommandSection": "commands",
"lastCommandRunTime": time.time(),
},
"input": {"curr_input": []},
"runtime": {
"DebugManager": Mock(write_debug_out=Mock()),
"OutputManager": output_manager,
"SettingsManager": settings_manager,
},
}
pty_driver.inject_text_to_screen = Mock()
pty_driver.handle_stdin_input(b"\x1b[7~", event_queue)
output_manager.interrupt_output.assert_not_called()
pty_driver.inject_text_to_screen.assert_not_called()
@pytest.mark.unit
def test_pty_recent_review_does_not_consume_plain_text_input():
pty_driver = PtyDriver()
event_queue = Mock()
settings_manager = Mock()
settings_manager.get_setting_as_bool.return_value = False
pty_driver.env = {
"commandInfo": {
"lastCommand": "REVIEW_PREV_LINE",
"lastCommandSection": "commands",
"lastCommandRunTime": time.time(),
},
"input": {"curr_input": []},
"runtime": {
"DebugManager": Mock(write_debug_out=Mock()),
"SettingsManager": settings_manager,
},
}
pty_driver.inject_text_to_screen = Mock()
pty_driver.handle_stdin_input(b"a", event_queue)
pty_driver.inject_text_to_screen.assert_called_once_with(b"a")
@pytest.mark.unit @pytest.mark.unit
def test_pty_backspace_with_fenrir_key_synthesizes_shortcut_events(): def test_pty_backspace_with_fenrir_key_synthesizes_shortcut_events():
pty_driver = PtyDriver() pty_driver = PtyDriver()