Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 447e2cc896 | |||
| 2b7d205f06 |
@@ -19,14 +19,14 @@ volume=0.7
|
||||
|
||||
# shell commands for generic sound driver
|
||||
# the folowing variable are substituted
|
||||
# fenrirVolume = the current volume setting
|
||||
# fenrirSoundFile = the soundfile for an soundicon
|
||||
# fenrirFrequence = the frequency to play
|
||||
# fenrirDuration = the duration of the frequency
|
||||
# fenrir_volume = the current volume setting
|
||||
# fenrir_sound_file = the soundfile for an soundicon
|
||||
# fenrir_frequency = the frequency to play
|
||||
# fenrir_duration = the duration of the frequency
|
||||
# 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
|
||||
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
|
||||
progress_monitoring=True
|
||||
@@ -114,17 +114,17 @@ max_batch_lines=100
|
||||
# Only enable flood control if this many new lines appear in the window
|
||||
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
|
||||
# fenrirText = is the text that should be spoken
|
||||
# fenrirModule = may be the speech module like used in speech-dispatcher, not every TTY need this
|
||||
# fenrirLanguage = the language
|
||||
# fenrirVoice = is the current voice that should be used. Set the voice variable above.
|
||||
# fenrir_text = is the text that should be spoken
|
||||
# fenrir_module = may be the speech module like used in speech-dispatcher, not every TTY need this
|
||||
# fenrir_language = the language
|
||||
# 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
|
||||
# value = min + settingValue * (min - max )
|
||||
# fenrirVolume = is replaced with the current volume
|
||||
# fenrirPitch = is replaced with the current pitch
|
||||
# fenrirRate = is replaced with the current speed (speech rate)
|
||||
# fenrir_volume = is replaced with the current volume
|
||||
# fenrir_pitch = is replaced with the current pitch
|
||||
# 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"
|
||||
|
||||
# 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
|
||||
# used path for "export_clipboard_to_file"
|
||||
# $user is replaced by username
|
||||
#clipboardExportPath=/home/$user/fenrirClipboard
|
||||
#clipboard_export_path=/home/$user/fenrirClipboard
|
||||
clipboard_export_path=/tmp/fenrirClipboard
|
||||
# Convert text emoticons like :) to descriptive text (e.g., "smiling face")
|
||||
emoticons=True
|
||||
|
||||
+12
-12
@@ -1506,38 +1506,38 @@ Values: `+0.0+` is quietest, `+1.0+` is loudest.
|
||||
The generic sound driver uses shell commands for play sound and
|
||||
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.
|
||||
|
||||
....
|
||||
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.
|
||||
|
||||
....
|
||||
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
|
||||
* `+fenrirSoundFile+` = the sound file for an sound icon
|
||||
* `+fenrirFrequence+` = the frequency to play
|
||||
* `+fenrirDuration+` = the duration of the frequency
|
||||
* `+fenrir_volume+` = the current volume setting
|
||||
* `+fenrir_sound_file+` = the sound file for an sound icon
|
||||
* `+fenrir_frequency+` = the frequency to play
|
||||
* `+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
|
||||
|
||||
+24
-24
@@ -857,21 +857,21 @@ Values: ''0.0'' is quietest, ''1.0'' is loudest.
|
||||
=== Generic Driver ===
|
||||
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>
|
||||
''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>
|
||||
|
||||
The following variables are substituted in ''genericPlayFileCommand'' and ''genericFrequencyCommand'':
|
||||
* ''fenrirVolume'' = the current volume setting
|
||||
* ''fenrirSoundFile'' = the sound file for an sound icon
|
||||
* ''fenrirFrequence'' = the frequency to play
|
||||
* ''fenrirDuration'' = the duration of the frequency
|
||||
The following variables are substituted in ''generic_play_file_command'' and ''generic_frequency_command'':
|
||||
* ''fenrir_volume'' = the current volume setting
|
||||
* ''fenrir_sound_file'' = the sound file for an sound icon
|
||||
* ''fenrir_frequency'' = the frequency to play
|
||||
* ''fenrir_duration'' = the duration of the frequency
|
||||
|
||||
Example genericPlayFileCommand (default)
|
||||
generic_play_file_command=play -q -v fenrirVolume fenrirSoundFile
|
||||
Example genericFrequencyCommand (default)
|
||||
generic_frequency_command=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence
|
||||
Example generic_play_file_command (default)
|
||||
generic_play_file_command=play -q -v fenrir_volume fenrir_sound_file
|
||||
Example generic_frequency_command (default)
|
||||
generic_frequency_command=play -q -v fenrir_volume -n -c1 synth fenrir_duration sine fenrir_frequency
|
||||
==== Speech ====
|
||||
Speech is configured in section ''[speech]''.
|
||||
Turn speech on or off:
|
||||
@@ -945,22 +945,22 @@ Values: on=''True'', off=''False''
|
||||
=== Generic Driver ===
|
||||
The generic speech driver uses shell commands for speech synthisus.
|
||||
|
||||
''genericSpeechCommand'' defines the command that is executed for creating speech
|
||||
The following variables are substituted in ''genericSpeechCommand'':
|
||||
* ''FenrirText'' = is the text that should be spoken
|
||||
* ''fenrirModule'' = may be the speech module like used in speech-dispatcher, not every TTY needs this
|
||||
* ''fenrirLanguage'' = the language to speak in
|
||||
* ''fenrirVoice'' = is the current voice that should be used
|
||||
* ''fenrirVolume'' = is replaced with the current volume
|
||||
* ''fenrirPitch'' = is replaced with the current pitch
|
||||
* ''fenrirRate'' = is replaced with the current speed (speech rate)
|
||||
''generic_speech_command'' defines the command that is executed for creating speech
|
||||
The following variables are substituted in ''generic_speech_command'':
|
||||
* ''fenrir_text'' = is the text that should be spoken
|
||||
* ''fenrir_module'' = may be the speech module like used in speech-dispatcher, not every TTY needs this
|
||||
* ''fenrir_language'' = the language to speak in
|
||||
* ''fenrir_voice'' = is the current voice that should be used
|
||||
* ''fenrir_volume'' = is replaced with the current volume
|
||||
* ''fenrir_pitch'' = is replaced with the current pitch
|
||||
* ''fenrir_rate'' = is replaced with the current speed (speech rate)
|
||||
|
||||
Example genericSpeechCommand (default):
|
||||
generic_speech_command=espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice "fenrirText"
|
||||
Example generic_speech_command (default):
|
||||
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_min_pitch=0
|
||||
fenrir_max_pitch=99
|
||||
|
||||
@@ -22,4 +22,7 @@ command_info = {
|
||||
# 'curr_command': '',
|
||||
"lastCommandExecutionTime": time.time(),
|
||||
"lastCommandRequestTime": time.time(),
|
||||
"lastCommand": "",
|
||||
"lastCommandSection": "",
|
||||
"lastCommandRunTime": 0,
|
||||
}
|
||||
|
||||
@@ -474,6 +474,10 @@ class CommandManager:
|
||||
def run_command(self, command, section="commands"):
|
||||
if self.command_exists(command, section):
|
||||
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(
|
||||
"run_command command:" + section + "." + command,
|
||||
debug.DebugLevel.INFO,
|
||||
|
||||
@@ -12,8 +12,13 @@ settings_data = {
|
||||
"driver": "genericDriver",
|
||||
"theme": "default",
|
||||
"volume": 1.0,
|
||||
"generic_play_file_command": "play -q -v fenrirVolume fenrirSoundFile",
|
||||
"generic_frequency_command": "play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence",
|
||||
"generic_play_file_command": (
|
||||
"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,
|
||||
},
|
||||
"speech": {
|
||||
@@ -38,7 +43,10 @@ settings_data = {
|
||||
"batch_flush_interval": 0.5,
|
||||
"max_batch_lines": 100,
|
||||
"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_max_volume": 200,
|
||||
"fenrir_min_pitch": 0,
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
version = "2026.06.01"
|
||||
code_name = "master"
|
||||
version = "2026.06.18"
|
||||
code_name = "testing"
|
||||
|
||||
@@ -304,6 +304,11 @@ class driver(screenDriver):
|
||||
"keyboard", "interrupt_on_key_press_filter"
|
||||
).strip():
|
||||
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()
|
||||
|
||||
def start_stdin_interrupt_thread(self):
|
||||
@@ -335,10 +340,43 @@ class driver(screenDriver):
|
||||
return
|
||||
if self.handle_vmenu_stdin_input(msg_bytes, event_queue):
|
||||
return
|
||||
if self.suppress_recent_review_stdin_tail(msg_bytes):
|
||||
return
|
||||
self.record_stdin_keypress(msg_bytes)
|
||||
self.interrupt_output_on_stdin_input(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):
|
||||
if not self.is_vmenu_active():
|
||||
return False
|
||||
|
||||
@@ -26,15 +26,15 @@ class driver(sound_driver):
|
||||
|
||||
Attributes:
|
||||
proc: Currently running subprocess for sound playback
|
||||
soundFileCommand (str): Command template for playing sound files
|
||||
frequenceCommand (str): Command template for generating frequencies
|
||||
sound_file_command (str): Command template for playing sound files
|
||||
frequency_command (str): Command template for generating frequencies
|
||||
"""
|
||||
def __init__(self):
|
||||
sound_driver.__init__(self)
|
||||
self.proc = None
|
||||
self.soundType = ""
|
||||
self.soundFileCommand = ""
|
||||
self.frequenceCommand = ""
|
||||
self.sound_type = ""
|
||||
self.sound_file_command = ""
|
||||
self.frequency_command = ""
|
||||
|
||||
def initialize(self, environment):
|
||||
"""Initialize the generic sound driver.
|
||||
@@ -46,17 +46,20 @@ class driver(sound_driver):
|
||||
environment: Fenrir environment dictionary with settings
|
||||
"""
|
||||
self.env = environment
|
||||
self.soundFileCommand = self.env["runtime"][
|
||||
self.sound_file_command = self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting("sound", "generic_play_file_command")
|
||||
self.frequenceCommand = self.env["runtime"][
|
||||
self.frequency_command = self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting("sound", "generic_frequency_command")
|
||||
if self.soundFileCommand == "":
|
||||
self.soundFileCommand = "play -q -v fenrirVolume fenrirSoundFile"
|
||||
if self.frequenceCommand == "":
|
||||
self.frequenceCommand = (
|
||||
"play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence"
|
||||
if self.sound_file_command == "":
|
||||
self.sound_file_command = (
|
||||
"play -q -v fenrir_volume fenrir_sound_file"
|
||||
)
|
||||
if self.frequency_command == "":
|
||||
self.frequency_command = (
|
||||
"play -q -v fenrir_volume -n -c1 synth "
|
||||
"fenrir_duration sine fenrir_frequency"
|
||||
)
|
||||
self._initialized = True
|
||||
|
||||
@@ -75,22 +78,22 @@ class driver(sound_driver):
|
||||
return
|
||||
if interrupt:
|
||||
self.cancel()
|
||||
popen_frequence_command = shlex.split(self.frequenceCommand)
|
||||
for idx, word in enumerate(popen_frequence_command):
|
||||
popen_frequency_command = shlex.split(self.frequency_command)
|
||||
for idx, word in enumerate(popen_frequency_command):
|
||||
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("fenrirFrequence", str(frequence))
|
||||
popen_frequence_command[idx] = word
|
||||
word = word.replace("fenrir_duration", str(duration))
|
||||
word = word.replace("fenrir_frequency", str(frequence))
|
||||
popen_frequency_command[idx] = word
|
||||
self.proc = subprocess.Popen(
|
||||
popen_frequence_command,
|
||||
popen_frequency_command,
|
||||
stdin=None,
|
||||
stdout=None,
|
||||
stderr=None,
|
||||
shell=False,
|
||||
)
|
||||
self.soundType = "frequence"
|
||||
self.sound_type = "frequence"
|
||||
|
||||
def play_sound_file(self, file_path, interrupt=True):
|
||||
"""Play a sound file.
|
||||
@@ -108,13 +111,13 @@ class driver(sound_driver):
|
||||
|
||||
if not os.path.isfile(file_path) or ".." in file_path:
|
||||
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):
|
||||
word = word.replace("fenrirVolume", str(self.volume))
|
||||
word = word.replace("fenrirSoundFile", shlex.quote(str(file_path)))
|
||||
word = word.replace("fenrir_volume", str(self.volume))
|
||||
word = word.replace("fenrir_sound_file", str(file_path))
|
||||
popen_sound_file_command[idx] = word
|
||||
self.proc = subprocess.Popen(popen_sound_file_command, shell=False)
|
||||
self.soundType = "file"
|
||||
self.sound_type = "file"
|
||||
|
||||
def cancel(self):
|
||||
"""Cancel currently playing sound.
|
||||
@@ -123,9 +126,9 @@ class driver(sound_driver):
|
||||
"""
|
||||
if not self._initialized:
|
||||
return
|
||||
if self.soundType == "":
|
||||
if self.sound_type == "":
|
||||
return
|
||||
if self.soundType == "file":
|
||||
if self.sound_type == "file":
|
||||
self.proc.kill()
|
||||
try:
|
||||
# Wait for process to finish to prevent zombies
|
||||
@@ -134,7 +137,7 @@ class driver(sound_driver):
|
||||
pass # Process already terminated
|
||||
except Exception as e:
|
||||
pass # Handle any other wait errors
|
||||
if self.soundType == "frequence":
|
||||
if self.sound_type == "frequence":
|
||||
self.proc.kill()
|
||||
try:
|
||||
# Wait for process to finish to prevent zombies
|
||||
@@ -143,4 +146,4 @@ class driver(sound_driver):
|
||||
pass # Process already terminated
|
||||
except Exception as e:
|
||||
pass # Handle any other wait errors
|
||||
self.soundType = ""
|
||||
self.sound_type = ""
|
||||
|
||||
@@ -30,49 +30,52 @@ class driver(speech_driver):
|
||||
def __init__(self):
|
||||
speech_driver.__init__(self)
|
||||
self.proc = None
|
||||
self.speechThread = Thread(target=self.worker)
|
||||
self.speech_thread = Thread(target=self.worker)
|
||||
self.lock = Lock()
|
||||
self.textQueue = SpeakQueue()
|
||||
self.text_queue = SpeakQueue()
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
self.minVolume = self.env["runtime"][
|
||||
self.min_volume = self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting_as_int("speech", "fenrir_min_volume")
|
||||
self.maxVolume = self.env["runtime"][
|
||||
self.max_volume = self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting_as_int("speech", "fenrir_max_volume")
|
||||
self.minPitch = self.env["runtime"][
|
||||
self.min_pitch = self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting_as_int("speech", "fenrir_min_pitch")
|
||||
self.maxPitch = self.env["runtime"][
|
||||
self.max_pitch = self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting_as_int("speech", "fenrir_max_pitch")
|
||||
self.minRate = self.env["runtime"][
|
||||
self.min_rate = self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting_as_int("speech", "fenrir_min_rate")
|
||||
self.maxRate = self.env["runtime"][
|
||||
self.max_rate = self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting_as_int("speech", "fenrir_max_rate")
|
||||
|
||||
self.speechCommand = self.env["runtime"][
|
||||
self.speech_command = self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting("speech", "generic_speech_command")
|
||||
if self.speechCommand == "":
|
||||
self.speechCommand = 'espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice -- "fenrirText"'
|
||||
if self.speech_command == "":
|
||||
self.speech_command = (
|
||||
"espeak -a fenrir_volume -s fenrir_rate -p fenrir_pitch "
|
||||
'-v fenrir_voice -- "fenrir_text"'
|
||||
)
|
||||
if False: # for debugging overwrite here
|
||||
# self.speechCommand = 'spd-say --wait -r 100 -i 100 "fenrirText"'
|
||||
self.speechCommand = 'flite -t "fenrirText"'
|
||||
# self.speech_command = 'spd-say --wait -r 100 -i 100 "fenrir_text"'
|
||||
self.speech_command = 'flite -t "fenrir_text"'
|
||||
|
||||
self._is_initialized = True
|
||||
if self._is_initialized:
|
||||
self.speechThread.start()
|
||||
self.speech_thread.start()
|
||||
|
||||
def shutdown(self):
|
||||
if not self._is_initialized:
|
||||
return
|
||||
self.cancel()
|
||||
self.textQueue.put(-1)
|
||||
self.text_queue.put(-1)
|
||||
|
||||
def speak(self, text, queueable=True, ignore_punctuation=False):
|
||||
if not self._is_initialized:
|
||||
@@ -88,7 +91,7 @@ class driver(speech_driver):
|
||||
"language": self.language,
|
||||
"voice": self.voice,
|
||||
}
|
||||
self.textQueue.put(utterance.copy())
|
||||
self.text_queue.put(utterance.copy())
|
||||
|
||||
def cancel(self):
|
||||
if not self._is_initialized:
|
||||
@@ -129,7 +132,7 @@ class driver(speech_driver):
|
||||
def clear_buffer(self):
|
||||
if not self._is_initialized:
|
||||
return
|
||||
self.textQueue.clear()
|
||||
self.text_queue.clear()
|
||||
|
||||
def set_voice(self, voice):
|
||||
if not self._is_initialized:
|
||||
@@ -140,13 +143,13 @@ class driver(speech_driver):
|
||||
if not self._is_initialized:
|
||||
return
|
||||
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):
|
||||
if not self._is_initialized:
|
||||
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):
|
||||
if not self._is_initialized:
|
||||
@@ -162,12 +165,29 @@ class driver(speech_driver):
|
||||
if not self._is_initialized:
|
||||
return
|
||||
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):
|
||||
while True:
|
||||
utterance = self.textQueue.get()
|
||||
utterance = self.text_queue.get()
|
||||
|
||||
if isinstance(utterance, int):
|
||||
if utterance == -1:
|
||||
@@ -209,21 +229,7 @@ class driver(speech_driver):
|
||||
if not isinstance(utterance["rate"], str):
|
||||
utterance["rate"] = ""
|
||||
|
||||
popen_speech_command = shlex.split(self.speechCommand)
|
||||
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
|
||||
popen_speech_command = self._build_speech_command(utterance)
|
||||
|
||||
try:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
@@ -70,6 +70,7 @@ def test_pty_stdin_input_interrupts_output_when_all_keys_interrupt_enabled():
|
||||
"runtime": {
|
||||
"SettingsManager": settings_manager,
|
||||
"OutputManager": output_manager,
|
||||
"DebugManager": Mock(write_debug_out=Mock()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +255,7 @@ def test_pty_stdin_input_honors_interrupt_disabled():
|
||||
"runtime": {
|
||||
"SettingsManager": settings_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": {
|
||||
"SettingsManager": settings_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()
|
||||
|
||||
|
||||
@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
|
||||
def test_pty_backspace_with_fenrir_key_synthesizes_shortcut_events():
|
||||
pty_driver = PtyDriver()
|
||||
|
||||
Reference in New Issue
Block a user