Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions accessible_output2/outputs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions accessible_output2/outputs/system_voiceover.py
Original file line number Diff line number Diff line change
@@ -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
38 changes: 28 additions & 10 deletions accessible_output2/outputs/voiceover.py
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions readme.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Speech:
- Supernova and other Dolphin products
- PC Talker
- Microsoft Speech API
- VoiceOver
- E-Speak


Braille:
Expand All @@ -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.