diff --git a/accessible_output2/outputs/__init__.py b/accessible_output2/outputs/__init__.py index 4bff6eb..354869d 100644 --- a/accessible_output2/outputs/__init__.py +++ b/accessible_output2/outputs/__init__.py @@ -33,6 +33,7 @@ def _load_com(*names): if platform.system() == "Darwin": from . import voiceover + from . import system_voiceover if platform.system() == "Linux": from . import speech_dispatcher diff --git a/accessible_output2/outputs/system_voiceover.py b/accessible_output2/outputs/system_voiceover.py new file mode 100644 index 0000000..bf29a2c --- /dev/null +++ b/accessible_output2/outputs/system_voiceover.py @@ -0,0 +1,69 @@ +from __future__ import absolute_import +import platform +from collections import OrderedDict + +from .base import Output, OutputError + +class SystemVoiceOver(Output): + """Default speech output supporting the Apple VoiceOver screen reader.""" + + name = "VoiceOver" + priority = 101 + system_output = True + + def __init__(self, *args, **kwargs): + from AppKit import NSSpeechSynthesizer + self.NSSpeechSynthesizer = NSSpeechSynthesizer + self.voiceover = NSSpeechSynthesizer.alloc().init() + self.voices = self._available_voices() + + def _available_voices(self): + voices = OrderedDict() + + for voice in self.NSSpeechSynthesizer.availableVoices(): + voice_attr = self.NSSpeechSynthesizer.attributesForVoice_(voice) + voice_name = voice_attr["VoiceName"] + voice_identifier = voice_attr["VoiceIdentifier"] + voices[voice_name] = voice_identifier + + return voices + + def list_voices(self): + return list(self.voices.keys()) + + def get_voice(self): + voice_attr = self.NSSpeechSynthesizer.attributesForVoice_(self.voiceover.voice()) + return voice_attr["VoiceName"] + + def set_voice(self, voice_name): + voice_identifier = self.voices[voice_name] + self.voiceover.setVoice_(voice_identifier) + + def get_rate(self): + return self.voiceover.rate() + + def set_rate(self, rate): + self.voiceover.setRate_(rate) + + def get_volume(self): + return self.voiceover.volume() + + def set_volume(self, volume): + self.voiceover.setVolume_(volume) + + def is_speaking(self): + return self.NSSpeechSynthesizer.isAnyApplicationSpeaking() + + def speak(self, text, interrupt=False): + if interrupt: + self.silence() + + return self.voiceover.startSpeakingString_(text) + + def silence(self): + self.voiceover.stopSpeaking() + + def is_active(self): + return self.voiceover is not None + +output_class = SystemVoiceOver \ No newline at end of file diff --git a/accessible_output2/outputs/voiceover.py b/accessible_output2/outputs/voiceover.py index 5c1de4b..6091eab 100644 --- a/accessible_output2/outputs/voiceover.py +++ b/accessible_output2/outputs/voiceover.py @@ -1,25 +1,43 @@ -from __future__ import absolute_import +import subprocess -from .base import Output +from accessible_output2.outputs.base import Output class VoiceOver(Output): - """Speech output supporting the Apple VoiceOver screen reader.""" name = "VoiceOver" def __init__(self, *args, **kwargs): - import appscript - self.app = appscript.app("voiceover") + from AppKit import NSSpeechSynthesizer + self.NSSpeechSynthesizer = NSSpeechSynthesizer + + def run_apple_script(self, command, process = "voiceover"): + return subprocess.Popen(["osascript", "-e", + f"tell application \"{process}\"\n{command}\nend tell"], + stdout = subprocess.PIPE).communicate()[0] + + def sanitize(self, str): + return str.replace("\\", "\\\\") \ + .replace("\"", "\\\"") def speak(self, text, interrupt=False): - self.app.output(text) + sanitized_text = self.sanitize(text) + # The silence function does not seem to work. + # osascript takes time to execute, so voiceover usually starts talking before being silenced + if interrupt: + self.silence() - def silence(self): - self.app.output(u"") + self.run_apple_script(f"output \"{sanitized_text}\"") - def is_active(self): - return self.app.isrunning() + def silence (self): + self.run_apple_script("output \"\"") + + def is_speaking(self): + return self.NSSpeechSynthesizer.isAnyApplicationSpeaking() + def is_active(self): + # If no process is found, an empty string is returned + return bool(subprocess.Popen(["pgrep", "-x", "VoiceOver"], + stdout = subprocess.PIPE).communicate()[0]) output_class = VoiceOver diff --git a/readme.rst b/readme.rst index 9774bf3..ff89cb5 100644 --- a/readme.rst +++ b/readme.rst @@ -25,6 +25,8 @@ Speech: - Supernova and other Dolphin products - PC Talker - Microsoft Speech API +- VoiceOver +- E-Speak Braille: @@ -35,3 +37,14 @@ Braille: - System Access - Supernova and other Dolphin products +Note for Apple Users: +------------------ +VoiceOver is supported by accessible_output2 in two different ways. + +The first way is through Apple Script, which requires the user to enable the VoiceOver setting "Allow Voiceover to be controled by Apple Script". This method will provide output to the running instance of voiceover. This no longer checks if VoiceOver has this setting enabled or not due to the expensive cost of running an Apple Script query everytime is_active is called. This means that if the VoiceOver setting is disabled, and VoiceOver is running, an error will be thrown by VoiceOver if you attempt to speak with VoiceOver, rather than automaticly switching to the secondary speech output system. Application developers that are providing support for VoiceOver are encouraged to provide some notification to the user about enabling Voiceover to be controled by Apple Script, or to just disable VoiceOver altogether to use the default speech output. + +If Voiceover is not running, The NSSpeechSynthesizer object is used. This will use a separate instance of VoiceOver, using default VoiceOver settings which are customizable from the provided class similar to SAPI5 for Windows. + +Error thrown by VoiceOver if Apple Script is disabled: (This error can not be caught in python ) + +execution error: VoiceOver got an error: AppleEvent handler failed.