diff --git a/src/accessible_output2/__init__.py b/src/accessible_output2/__init__.py index 311513a..c826e7a 100644 --- a/src/accessible_output2/__init__.py +++ b/src/accessible_output2/__init__.py @@ -1,19 +1,23 @@ +from __future__ import absolute_import import ctypes import os import types from platform_utils import paths -def load_library(libname): +def load_library(libname, cdll=False): if paths.is_frozen(): libfile = os.path.join(paths.embedded_data_path(), 'accessible_output2', 'lib', libname) else: libfile = os.path.join(paths.module_path(), 'lib', libname) - return ctypes.windll[libfile] + if cdll: + return ctypes.cdll[libfile] + else: + return ctypes.windll[libfile] def get_output_classes(): - import outputs + from . import outputs module_type = types.ModuleType - classes = [m.output_class for m in outputs.__dict__.itervalues() if type(m) == module_type and hasattr(m, 'output_class')] + classes = [m.output_class for m in outputs.__dict__.values() if type(m) == module_type and hasattr(m, 'output_class')] return sorted(classes, key=lambda c: c.priority) def find_datafiles(): diff --git a/src/accessible_output2/lib/SAAPI32.dll b/src/accessible_output2/lib/SAAPI32.dll index 0384e7d..166f7d3 100644 Binary files a/src/accessible_output2/lib/SAAPI32.dll and b/src/accessible_output2/lib/SAAPI32.dll differ diff --git a/src/accessible_output2/lib/jfwapi.dll b/src/accessible_output2/lib/jfwapi.dll deleted file mode 100644 index e052823..0000000 Binary files a/src/accessible_output2/lib/jfwapi.dll and /dev/null differ diff --git a/src/accessible_output2/outputs/__init__.py b/src/accessible_output2/outputs/__init__.py index f195efd..d0b9a48 100644 --- a/src/accessible_output2/outputs/__init__.py +++ b/src/accessible_output2/outputs/__init__.py @@ -1,14 +1,20 @@ +from __future__ import absolute_import import platform if platform.system() == 'Windows': - import nvda - import jaws - import window_eyes - import system_access - import dolphin - import pc_talker -elif platform.system() == "Darwin": - import voiceover -elif platform.system() == "Linux": - import speechDispatcher + from . import nvda + from . import jaws + from . import sapi5 + from . import window_eyes + from . import system_access + from . import dolphin + from . import pc_talker + #import sapi4 -import auto +if platform.system() == 'Darwin': + from . import voiceover + from . import say + +if platform.system() == 'Linux': + from . import e_speak + +from . import auto diff --git a/src/accessible_output2/outputs/auto.py b/src/accessible_output2/outputs/auto.py index 808f8f5..8026fce 100644 --- a/src/accessible_output2/outputs/auto.py +++ b/src/accessible_output2/outputs/auto.py @@ -1,24 +1,17 @@ -import platform +from __future__ import absolute_import import accessible_output2 -from base import Output, OutputError +from .base import Output, OutputError class Auto(Output): def __init__(self): - if platform.system() == "Darwin": - import voiceover - self.outputs = [voiceover.VoiceOver()] - elif platform.system() == "Linux": - import speechDispatcher - self.outputs = [speechDispatcher.SpeechDispatcher()] - elif platform.system() == "Windows": - output_classes = accessible_output2.get_output_classes() - self.outputs = [] - for output in output_classes: - try: - self.outputs.append(output()) - except OutputError: - pass + output_classes = accessible_output2.get_output_classes() + self.outputs = [] + for output in output_classes: + try: + self.outputs.append(output()) + except OutputError: + pass def get_first_available_output(self): for output in self.outputs: @@ -40,3 +33,8 @@ class Auto(Output): output = self.get_first_available_output() if output: output.speak(*args, **kwargs) + + def is_system_output(self): + output = self.get_first_available_output() + if output: + return output.is_system_output() diff --git a/src/accessible_output2/outputs/base.py b/src/accessible_output2/outputs/base.py index f08266b..2847f2f 100644 --- a/src/accessible_output2/outputs/base.py +++ b/src/accessible_output2/outputs/base.py @@ -5,27 +5,43 @@ class OutputError(Exception): pass class Output(object): - name = "Unnamed Output" #The name of this output - lib32 = None #name of 32-bit lib - lib64 = None #name of 64-bit lib - priority = 100 #Where to sort in the list of available outputs for automaticly speaking + name = "Unnamed Output" + lib32 = None + lib64 = None + argtypes = {} + cdll = False + priority = 100 + system_output = False def __init__(self): - is_32bit = platform.architecture()[0] == "32bit" - if self.lib32 and is_32bit: - self.lib = load_library(self.lib32) + self.is_32bit = platform.architecture()[0] == "32bit" + if self.lib32 and self.is_32bit: + self.lib = load_library(self.lib32, cdll=self.cdll) elif self.lib64: - self.lib = load_library(self.lib64) + self.lib = load_library(self.lib64, cdll=self.cdll) + else: + self.lib = None + if self.lib is not None: + for func in self.argtypes: + try: + getattr(self.lib, func).argtypes = self.argtypes[func] + except AttributeError: + pass def output(self, text, **options): output = False - if hasattr(self, 'speak') and callable(self.speak): - self.speak(text, **options) + if self.speak(text, **options): output = True - if hasattr(self, 'braille') and callable(self.braille): - self.braille(text, **options) + if self.braille(text, **options): output = True if not output: raise RuntimeError("Output %r does not have any method defined to output" % self) + def is_system_output(self): + return self.system_output + def speak(self, **optiont): + return False + + def braille(self, *args, **options): + return False diff --git a/src/accessible_output2/outputs/dolphin.py b/src/accessible_output2/outputs/dolphin.py index f9ac82c..4d4efb6 100644 --- a/src/accessible_output2/outputs/dolphin.py +++ b/src/accessible_output2/outputs/dolphin.py @@ -1,19 +1,25 @@ +from __future__ import absolute_import import os +import ctypes -from base import Output +from .base import Output class Dolphin (Output): """Supports dolphin products.""" name = 'Dolphin' lib32 = 'dolapi.dll' + argtypes = { + 'DolAccess_Command': (ctypes.c_wchar_p, ctypes.c_int, ctypes.c_int), + 'DolAccess_Action': (ctypes.c_int,), + } def speak(self, text, interrupt=0): if interrupt: self.silence() #If we don't call this, the API won't let us speak. if self.is_active(): - self.lib.DolAccess_Command(unicode(text), (len(text)*2)+2, 1) + self.lib.DolAccess_Command(text, (len(text)*2)+2, 1) def silence(self): self.lib.DolAccess_Action(141) diff --git a/src/accessible_output2/outputs/e_speak.py b/src/accessible_output2/outputs/e_speak.py new file mode 100644 index 0000000..6c60b95 --- /dev/null +++ b/src/accessible_output2/outputs/e_speak.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import +from .base import Output + +try: + import espeak.core +except: + raise RuntimeError("Cannot find espeak.core. Please install python-espeak") + +class ESpeak(Output): + """Speech output supporting ESpeak on Linux + Note this requires python-espeak to be installed + This can be done on Debian distros by using apt-get install python-espeak + Or through this tarball: https://launchpad.net/python-espeak + """ + name = "Linux ESpeak" + + def is_active(self): + try: + import espeak.core + except: + return False + return True + + def speak(self, text, interrupt = 0): + if interrupt: + self.silence() + espeak.core.synth(text) + def silence(self): + espeak.core.cancel() + +output_class = ESpeak diff --git a/src/accessible_output2/outputs/jaws.py b/src/accessible_output2/outputs/jaws.py index 90f34fb..18a38df 100644 --- a/src/accessible_output2/outputs/jaws.py +++ b/src/accessible_output2/outputs/jaws.py @@ -1,8 +1,9 @@ +from __future__ import absolute_import import win32gui from libloader.com import load_com import pywintypes -from base import Output, OutputError +from .base import Output, OutputError class Jaws (Output): """Output supporting the Jaws for Windows screen reader.""" diff --git a/src/accessible_output2/outputs/nvda.py b/src/accessible_output2/outputs/nvda.py index a6f373e..5affba3 100644 --- a/src/accessible_output2/outputs/nvda.py +++ b/src/accessible_output2/outputs/nvda.py @@ -1,15 +1,21 @@ +from __future__ import absolute_import import os import platform +import ctypes from platform_utils import paths from libloader import load_library -from base import Output +from .base import Output class NVDA(Output): """Supports The NVDA screen reader""" name = "NVDA" lib32 = 'nvdaControllerClient32.dll' lib64 = 'nvdaControllerClient64.dll' + argtypes = { + 'nvdaController_brailleMessage': (ctypes.c_wchar_p,), + 'nvdaController_speakText': (ctypes.c_wchar_p,), + } def is_active(self): try: @@ -18,12 +24,12 @@ class NVDA(Output): return False def braille(self, text, **options): - self.lib.nvdaController_brailleMessage(unicode(text)) + self.lib.nvdaController_brailleMessage(text) def speak(self, text, interrupt=False): if interrupt: self.silence() - self.lib.nvdaController_speakText(unicode(text)) + self.lib.nvdaController_speakText(text) def silence(self): self.lib.nvdaController_cancelSpeech() diff --git a/src/accessible_output2/outputs/pc_talker.py b/src/accessible_output2/outputs/pc_talker.py index 66bafb3..ebdb043 100644 --- a/src/accessible_output2/outputs/pc_talker.py +++ b/src/accessible_output2/outputs/pc_talker.py @@ -1,14 +1,19 @@ +from __future__ import absolute_import import ctypes -from base import Output +from .base import Output class PCTalker(Output): lib32 = 'pctkusr.dll' lib64 = 'pctkusr64.dll' + cdll = True + argtypes = { + 'PCTKPRead': (ctypes.c_char_p, ctypes.c_int, ctypes.c_int) + } def speak(self, text, interrupt=False): if interrupt: self.silence() - self.lib.PCTKPRead(text.encode('cp932', 'replace')) + self.lib.PCTKPRead(text.encode('cp932', 'replace'), 0, 1) def silence(self): self.lib.PCTKVReset() diff --git a/src/accessible_output2/outputs/sapi4.py b/src/accessible_output2/outputs/sapi4.py new file mode 100644 index 0000000..8f4c215 --- /dev/null +++ b/src/accessible_output2/outputs/sapi4.py @@ -0,0 +1,143 @@ +from __future__ import absolute_import +from builtins import range +from libloader.com import load_com +from .base import Output + +import logging +log = logging.getLogger(__name__) + +class Sapi4(Output): + + name = 'sapi4' + priority = 102 + + def __init__(self): + sapi4 = load_com("{EEE78591-FE22-11D0-8BEF-0060081841DE}") + self._voiceNo = sapi4.Find(0) + sapi4.Select(self._voiceNo) + sapi4.Speak(" ") + self.__object = sapi4 + self._voice_list = self._available_voices() + + def _set_capabilities(self): + sapi4 = self.__object + try: + sapi4.Pitch = sapi4.Pitch + self._min_pitch = sapi4.MinPitch + self._max_pitch = sapi4.MaxPitch + self._has_pitch = True + except: + self._min_pitch = 0 + self._max_pitch = 0 + self._has_pitch = False + try: + sapi4.Speed = sapi4.Speed + self._min_rate = sapi4.MinSpeed + self._max_rate = sapi4.MaxSpeed + self._has_rate = True + except: + self._min_rate = 0 + self._max_rate = 0 + self._has_rate = False + try: + sapi4.VolumeLeft = sapi4.VolumeLeft + self._min_volume = sapi4.MinVolumeLeft + self._max_volume = sapi4.MaxVolumeLeft + self._has_volume = True + except: + self._min_volume = 0 + self._max_volume = 0 + self._has_volume = False + + def _available_voices(self): + voice_list = [] + for voice_no in range(1, self.__object.CountEngines): + voice_list.append(self.__object.ModeName(voice_no)) + return voice_list + + @property + def available_voices(self): + return self._voice_list + + def list_voices(self): + return self.available_voices + + def get_voice(self): + return self.__object.ModeName(self._voice_no) + + def set_voice(self, value): + self._voice_no = self.list_voices().index(value) + 1 + self.__object.Select(self._voice_no) + self.silence() + self.__object.Speak(" ") + self._set_capabilities() + + def get_pitch(self): + if self.has_pitch: + return self.__object.Pitch + + def set_pitch(self, value): + if self.has_pitch: + self.__object.Pitch = value + + def get_rate(self): + if self.has_rate: + return self.__object.Speed + + def set_rate(self, value): + if self.has_rate: + self.__object.Speed = value + + def get_volume(self): + if self.has_volume: + return self.__object.VolumeLeft + + def set_volume(self, value): + if self.has_volume: + self.__object.VolumeLeft = value + + @property + def has_pitch(self): + return self._has_pitch + + @property + def has_rate(self): + return self._has_rate + + @property + def has_volume(self): + return self._has_volume + + @property + def min_pitch(self): + return self._min_pitch + + @property + def max_pitch(self): + return self._max_pitch + + @property + def min_rate(self): + return self._min_rate + + @property + def max_rate(self): + return self._max_rate + + @property + def min_volume(self): + return self._min_volume + + @property + def max_volume(self): + return self._max_volume + + def speak(self, text, interrupt=False): + if interrupt: + self.silence() + self.__object.Speak(text) + + def silence(self): + self.__object.AudioReset() + +output_class = Sapi4 diff --git a/src/accessible_output2/outputs/sapi5.py b/src/accessible_output2/outputs/sapi5.py new file mode 100644 index 0000000..f75ab2a --- /dev/null +++ b/src/accessible_output2/outputs/sapi5.py @@ -0,0 +1,95 @@ +from __future__ import absolute_import +from collections import OrderedDict +from libloader.com import load_com +from .base import Output, OutputError +import pywintypes +import logging +log = logging.getLogger(__name__) + +SVSFDefault = 0 +SVSFlagsAsync = 1 +SVSFPurgeBeforeSpeak = 2 +SVSFIsFilename = 4 +SVSFIsXML = 8 +SVSFIsNotXML = 16 +SVSFPersistXML = 32 + +class SAPI5(Output): + has_volume = True + has_rate = True + has_pitch = True + min_pitch = -10 + max_pitch = 10 + min_rate = -10 + max_rate = 10 + min_volume = 0 + max_volume = 100 + name = "sapi5" + priority = 101 + system_output = True + + def __init__(self): + try: + self.object = load_com("SAPI.SPVoice") + self._voices = self._available_voices() + except pywintypes.com_error: + raise OutputError + self._pitch = 0 + + def _available_voices(self): + _voices = OrderedDict() + for v in self.object.GetVoices(): + _voices[v.GetDescription()] = v + return _voices + + def list_voices(self): + return list(self._voices.keys()) + + def get_voice(self): + return self.object.Voice.GetDescription() + + def set_voice(self, value): + log.debug("Setting SAPI5 voice to \"%s\"" % value) + self.object.Voice = self._voices[value] + # For some reason SAPI5 does not reset audio after changing the voice + # By setting the audio device after changing voices seems to fix this + # This was noted from information at: + # http://lists.nvaccess.org/pipermail/nvda-dev/2011-November/022464.html + self.object.AudioOutput = self.object.AudioOutput + + def get_pitch(self): + return self._pitch + + def set_pitch(self, value): + log.debug("Setting pitch to %d" % value) + self._pitch = value + + def get_rate(self): + return self.object.Rate + + def set_rate(self, value): + log.debug("Setting rate to %d" % value) + self.object.Rate = value + + def get_volume(self): + return self.object.Volume + + def set_volume(self, value): + self.object.Volume = value + + def speak(self, text, interrupt=False): + if interrupt: + self.silence() + # We need to do the pitch in XML here + textOutput = "%s" % (round(self._pitch), text.replace("<", "<")) + self.object.Speak(textOutput, SVSFlagsAsync | SVSFIsXML) + + def silence(self): + self.object.Speak("", SVSFlagsAsync | SVSFPurgeBeforeSpeak) + + def is_active(self): + if self.object: + return True + return False + +output_class = SAPI5 diff --git a/src/accessible_output2/outputs/say.py b/src/accessible_output2/outputs/say.py new file mode 100644 index 0000000..5eb3bc4 --- /dev/null +++ b/src/accessible_output2/outputs/say.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import +import os +from .base import Output + +class AppleSay(Output): + """Speech output supporting the Apple Say subsystem.""" + name = 'Apple Say' + def __init__(self, voice = 'Alex', rate = '300'): + self.voice = voice + self.rate = rate + super(AppleSay, self).__init__() + def is_active(self): + return not os.system('which say') + def speak(self, text, interrupt = 0): + if interrupt: + self.silence() + os.system('say -v %s -r %s "%s" &' % (self.voice, self.rate, text)) + def silence(self): + os.system('killall say') + +output_class = AppleSay \ No newline at end of file diff --git a/src/accessible_output2/outputs/speechDispatcher.py b/src/accessible_output2/outputs/speechDispatcher.py deleted file mode 100644 index 1509ecd..0000000 --- a/src/accessible_output2/outputs/speechDispatcher.py +++ /dev/null @@ -1,29 +0,0 @@ -from base import Output, OutputError -import atexit -import application -class SpeechDispatcher(Output): - """Supports speech dispatcher on Linux. - Note that this module will use the configuration of speech dispatcher, the user will need to configure the voice, language, punctuation and rate before using this module. - """ - name = 'SpeechDispatcher' - - def __init__(self, *args, **kwargs): - super(SpeechDispatcher, self).__init__(*args, **kwargs) - try: - import speechd - self.spd = speechd.SSIPClient(application.name) - except ImportError: - raise OutputError - atexit.register(self.on_exit_event) - - def speak(self, text, interupt=False): - if interupt == True: - self.spd.cancel() - self.spd.speak(text) - - def is_active(self): - return True - - def on_exit_event(self): - self.spd.close() - del self.spd diff --git a/src/accessible_output2/outputs/speechd/__init__.py b/src/accessible_output2/outputs/speechd/__init__.py deleted file mode 100644 index 5b59f0c..0000000 --- a/src/accessible_output2/outputs/speechd/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (C) 2001, 2002 Brailcom, o.p.s. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - -from .client import * - diff --git a/src/accessible_output2/outputs/speechd/client.py b/src/accessible_output2/outputs/speechd/client.py deleted file mode 100644 index 8d34363..0000000 --- a/src/accessible_output2/outputs/speechd/client.py +++ /dev/null @@ -1,1125 +0,0 @@ -# Copyright (C) 2003-2008 Brailcom, o.p.s. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - -"""Python API to Speech Dispatcher - -Basic Python client API to Speech Dispatcher is provided by the 'SSIPClient' -class. This interface maps directly to available SSIP commands and logic. - -A more convenient interface is provided by the 'Speaker' class. - -""" - -#TODO: Blocking variants for speak, char, key, sound_icon. - -import socket, sys, os, subprocess, time, tempfile - -try: - import threading -except: - import dummy_threading as threading - -from . import paths - -class CallbackType(object): - """Constants describing the available types of callbacks""" - INDEX_MARK = 'index_marks' - """Index mark events are reported when the place they were - included into the text by the client application is reached - when speaking them""" - BEGIN = 'begin' - """The begin event is reported when Speech Dispatcher starts - actually speaking the message.""" - END = 'end' - """The end event is reported after the message has terminated and - there is no longer any sound from it being produced""" - CANCEL = 'cancel' - """The cancel event is reported when a message is canceled either - on request of the user, because of prioritization of messages or - due to an error""" - PAUSE = 'pause' - """The pause event is reported after speaking of a message - was paused. It no longer produces any audio.""" - RESUME = 'resume' - """The resume event is reported right after speaking of a message - was resumed after previous pause.""" - -class SSIPError(Exception): - """Common base class for exceptions during SSIP communication.""" - -class SSIPCommunicationError(SSIPError): - """Exception raised when trying to operate on a closed connection.""" - - _additional_exception = None - - def __init__(self, description=None, original_exception=None, **kwargs): - self._original_exception = original_exception - self._description = description - super(SSIPError, self).__init__(**kwargs) - - def original_exception(self): - """Return the original exception if any - - If this exception is secondary, being caused by a lower - level exception, return this original exception, otherwise - None""" - return self._original_exception - - def set_additional_exception(self, exception): - """Set an additional exception - - See method additional_exception(). - """ - self._additional_exception = exception - - def additional_exception(self): - """Return an additional exception - - Additional exceptions araise from failed attempts to resolve - the former problem""" - return self._additional_exception - - def description(self): - """Return error description""" - return self._description - - def __str__(self): - msgs = [] - if self.description(): - msgs.append(self.description()) - if self.original_exception: - msgs.append("Original error: " + str(self.original_exception())) - if self.additional_exception: - msgs.append("Additional error: " + str(self.additional_exception())) - return "\n".join(msgs) - -class SSIPResponseError(Exception): - def __init__(self, code, msg, data): - Exception.__init__(self, "%s: %s" % (code, msg)) - self._code = code - self._msg = msg - self._data = data - - def code(self): - """Return the server response error code as integer number.""" - return self._code - - def msg(self): - """Return server response error message as string.""" - return self._msg - - -class SSIPCommandError(SSIPResponseError): - """Exception raised on error response after sending command.""" - - def command(self): - """Return the command string which resulted in this error.""" - return self._data - - -class SSIPDataError(SSIPResponseError): - """Exception raised on error response after sending data.""" - - def data(self): - """Return the data which resulted in this error.""" - return self._data - - -class SpawnError(Exception): - """Indicates failure in server autospawn.""" - -class CommunicationMethod(object): - """Constants describing the possible methods of connection to server.""" - UNIX_SOCKET = 'unix_socket' - """Unix socket communication using a filesystem path""" - INET_SOCKET = 'inet_socket' - """Inet socket communication using a host and port""" - -class _SSIP_Connection(object): - """Implemantation of low level SSIP communication.""" - - _NEWLINE = b"\r\n" - _END_OF_DATA_MARKER = b'.' - _END_OF_DATA_MARKER_ESCAPED = b'..' - _END_OF_DATA = _NEWLINE + _END_OF_DATA_MARKER + _NEWLINE - _END_OF_DATA_ESCAPED = _NEWLINE + _END_OF_DATA_MARKER_ESCAPED + _NEWLINE - # Constants representing \r\n. and \r\n.. - _RAW_DOTLINE = _NEWLINE + _END_OF_DATA_MARKER - _ESCAPED_DOTLINE = _NEWLINE + _END_OF_DATA_MARKER_ESCAPED - - _CALLBACK_TYPE_MAP = {700: CallbackType.INDEX_MARK, - 701: CallbackType.BEGIN, - 702: CallbackType.END, - 703: CallbackType.CANCEL, - 704: CallbackType.PAUSE, - 705: CallbackType.RESUME, - } - - def __init__(self, communication_method, socket_path, host, port): - """Init connection: open the socket to server, - initialize buffers, launch a communication handling - thread. - """ - - if communication_method == CommunicationMethod.UNIX_SOCKET: - socket_family = socket.AF_UNIX - socket_connect_args = socket_path - elif communication_method == CommunicationMethod.INET_SOCKET: - assert host and port - socket_family = socket.AF_INET - socket_connect_args = (socket.gethostbyname(host), port) - else: - raise ValueError("Unsupported communication method") - - try: - self._socket = socket.socket(socket_family, socket.SOCK_STREAM) - self._socket.connect(socket_connect_args) - except socket.error as ex: - raise SSIPCommunicationError("Can't open socket using method " - + communication_method, - original_exception = ex) - - self._buffer = b"" - self._com_buffer = [] - self._callback = None - self._ssip_reply_semaphore = threading.Semaphore(0) - self._communication_thread = \ - threading.Thread(target=self._communication, kwargs={}, - name="SSIP client communication thread") - self._communication_thread.start() - - def close(self): - """Close the server connection, destroy the communication thread.""" - # Read-write shutdown here is necessary, otherwise the socket.recv() - # function in the other thread won't return at last on some platforms. - try: - self._socket.shutdown(socket.SHUT_RDWR) - except socket.error: - pass - self._socket.close() - # Wait for the other thread to terminate - self._communication_thread.join() - - def _communication(self): - """Handle incomming socket communication. - - Listens for all incomming communication on the socket, dispatches - events and puts all other replies into self._com_buffer list in the - already parsed form as (code, msg, data). Each time a new item is - appended to the _com_buffer list, the corresponding semaphore - 'self._ssip_reply_semaphore' is incremented. - - This method is designed to run in a separate thread. The thread can be - interrupted by closing the socket on which it is listening for - reading.""" - - while True: - try: - code, msg, data = self._recv_message() - except IOError: - # If the socket has been closed, exit the thread - sys.exit() - if code//100 != 7: - # This is not an index mark nor an event - self._com_buffer.append((code, msg, data)) - self._ssip_reply_semaphore.release() - continue - # Ignore the event if no callback function has been registered. - if self._callback is not None: - type = self._CALLBACK_TYPE_MAP[code] - if type == CallbackType.INDEX_MARK: - kwargs = {'index_mark': data[2]} - else: - kwargs = {} - # Get message and client ID of the event - msg_id, client_id = map(int, data[:2]) - self._callback(msg_id, client_id, type, **kwargs) - - - def _readline(self): - """Read one whole line from the socket. - - Blocks until the line delimiter ('_NEWLINE') is read. - - """ - pointer = self._buffer.find(self._NEWLINE) - while pointer == -1: - try: - d = self._socket.recv(1024) - except: - raise IOError - if len(d) == 0: - raise IOError - self._buffer += d - pointer = self._buffer.find(self._NEWLINE) - line = self._buffer[:pointer] - self._buffer = self._buffer[pointer+len(self._NEWLINE):] - return line.decode('utf-8') - - def _recv_message(self): - """Read server response or a callback - and return the triplet (code, msg, data).""" - data = [] - c = None - while True: - line = self._readline() - assert len(line) >= 4, "Malformed data received from server!" - code, sep, text = line[:3], line[3], line[4:] - assert code.isalnum() and (c is None or code == c) and \ - sep in ('-', ' '), "Malformed data received from server!" - if sep == ' ': - msg = text - return int(code), msg, tuple(data) - data.append(text) - - def _recv_response(self): - """Read server response from the communication thread - and return the triplet (code, msg, data).""" - # TODO: This check is dumb but seems to work. The main thread - # hangs without it, when the Speech Dispatcher connection is lost. - if not self._communication_thread.isAlive(): - raise SSIPCommunicationError - self._ssip_reply_semaphore.acquire() - # The list is sorted, read the first item - response = self._com_buffer[0] - del self._com_buffer[0] - return response - - def send_command(self, command, *args): - """Send SSIP command with given arguments and read server response. - - Arguments can be of any data type -- they are all stringified before - being sent to the server. - - Returns a triplet (code, msg, data), where 'code' is a numeric SSIP - response code as an integer, 'msg' is an SSIP rsponse message as string - and 'data' is a tuple of strings (all lines of response data) when a - response contains some data. - - 'SSIPCommandError' is raised in case of non 2xx return code. See SSIP - documentation for more information about server responses and codes. - - 'IOError' is raised when the socket was closed by the remote side. - - """ - if __debug__: - if command in ('SET', 'CANCEL', 'STOP',): - assert args[0] in (Scope.SELF, Scope.ALL) \ - or isinstance(args[0], int) - cmd = ' '.join((command,) + tuple(map(str, args))) - try: - self._socket.send(cmd.encode('utf-8') + self._NEWLINE) - except socket.error: - raise SSIPCommunicationError("Speech Dispatcher connection lost.") - code, msg, data = self._recv_response() - if code//100 != 2: - raise SSIPCommandError(code, msg, cmd) - return code, msg, data - - def send_data(self, data): - """Send multiline data and read server response. - - Returned value is the same as for 'send_command()' method. - - 'SSIPDataError' is raised in case of non 2xx return code. See SSIP - documentation for more information about server responses and codes. - - 'IOError' is raised when the socket was closed by the remote side. - - """ - data = data.encode('utf-8') - # Escape the end-of-data marker even if present at the beginning - # The start of the string is also the start of a line. - if data.startswith(self._END_OF_DATA_MARKER): - l = len(self._END_OF_DATA_MARKER) - data = self._END_OF_DATA_MARKER_ESCAPED + data[l:] - - # Escape the end of data marker at the start of each subsequent - # line. We can do that by simply replacing \r\n. with \r\n.., - # since the start of a line is immediately preceded by \r\n, - # when the line is not the beginning of the string. - data = data.replace(self._RAW_DOTLINE, self._ESCAPED_DOTLINE) - - try: - self._socket.send(data + self._END_OF_DATA) - except socket.error: - raise SSIPCommunicationError("Speech Dispatcher connection lost.") - code, msg, response_data = self._recv_response() - if code//100 != 2: - raise SSIPDataError(code, msg, data) - return code, msg, response_data - - def set_callback(self, callback): - """Register a callback function for handling asynchronous events. - - Arguments: - callback -- a callable object (function) which will be called to - handle asynchronous events (arguments described below). Passing - `None' results in removing the callback function and ignoring - events. Just one callback may be registered. Attempts to register - a second callback will result in the former callback being - replaced. - - The callback function must accept three positional arguments - ('message_id', 'client_id', 'event_type') and an optional keyword - argument 'index_mark' (when INDEX_MARK events are turned on). - - Note, that setting the callback function doesn't turn the events on. - The user is responsible to turn them on by sending the appropriate `SET - NOTIFICATION' command. - - """ - self._callback = callback - -class _CallbackHandler(object): - """Internal object which handles callbacks.""" - - def __init__(self, client_id): - self._client_id = client_id - self._callbacks = {} - self._lock = threading.Lock() - - def __call__(self, msg_id, client_id, type, **kwargs): - if client_id != self._client_id: - # TODO: does that ever happen? - return - self._lock.acquire() - try: - try: - callback, event_types = self._callbacks[msg_id] - except KeyError: - pass - else: - if event_types is None or type in event_types: - callback(type, **kwargs) - if type in (CallbackType.END, CallbackType.CANCEL): - del self._callbacks[msg_id] - finally: - self._lock.release() - - def add_callback(self, msg_id, callback, event_types): - self._lock.acquire() - try: - self._callbacks[msg_id] = (callback, event_types) - finally: - self._lock.release() - -class Scope(object): - """An enumeration of valid SSIP command scopes. - - The constants of this class should be used to specify the 'scope' argument - for the 'Client' methods. - - """ - SELF = 'self' - """The command (mostly a setting) applies to current connection only.""" - ALL = 'all' - """The command applies to all current Speech Dispatcher connections.""" - - -class Priority(object): - """An enumeration of valid SSIP message priorities. - - The constants of this class should be used to specify the 'priority' - argument for the 'Client' methods. For more information about message - priorities and their interaction, see the SSIP documentation. - - """ - IMPORTANT = 'important' - TEXT = 'text' - MESSAGE = 'message' - NOTIFICATION = 'notification' - PROGRESS = 'progress' - - -class PunctuationMode(object): - """Constants for selecting a punctuation mode. - - The mode determines which characters should be read. - - """ - ALL = 'all' - """Read all punctuation characters.""" - NONE = 'none' - """Don't read any punctuation character at all.""" - SOME = 'some' - """Only the user-defined punctuation characters are read. - - The set of characters is specified in Speech Dispatcher configuration. - - """ - -class DataMode(object): - """Constants specifying the type of data contained within messages - to be spoken. - - """ - TEXT = 'text' - """Data is plain text.""" - SSML = 'ssml' - """Data is SSML (Speech Synthesis Markup Language).""" - - -class SSIPClient(object): - """Basic Speech Dispatcher client interface. - - This class provides a Python interface to Speech Dispatcher functionality - over an SSIP connection. The API maps directly to available SSIP commands. - Each connection to Speech Dispatcher is represented by one instance of this - class. - - Many commands take the 'scope' argument, thus it is shortly documented - here. It is either one of 'Scope' constants or a number of connection. By - specifying the connection number, you are applying the command to a - particular connection. This feature is only meant to be used by Speech - Dispatcher control application, however. More datails can be found in - Speech Dispatcher documentation. - - """ - - DEFAULT_HOST = '127.0.0.1' - """Default host for server connections.""" - DEFAULT_PORT = 6560 - """Default port number for server connections.""" - DEFAULT_SOCKET_PATH = "speech-dispatcher/speechd.sock" - """Default name of the communication unix socket""" - - def __init__(self, name, component='default', user='unknown', address=None, - autospawn=None, - # Deprecated -> - host=None, port=None, method=None, socket_path=None): - """Initialize the instance and connect to the server. - - Arguments: - name -- client identification string - component -- connection identification string. When one client opens - multiple connections, this can be used to identify each of them. - user -- user identification string (user name). When multi-user - acces is expected, this can be used to identify their connections. - address -- server address as specified in Speech Dispatcher - documentation (e.g. "unix:/run/user/joe/speech-dispatcher/speechd.sock" - or "inet:192.168.0.85:6561") - autospawn -- a flag to specify whether the library should - try to start the server if it determines its not already - running or not - - Deprecated arguments: - method -- communication method to use, one of the constants defined in class - CommunicationMethod - socket_path -- for CommunicationMethod.UNIX_SOCKET, socket - path in filesystem. By default, this is $XDG_RUNTIME_DIR/speech-dispatcher/speechd.sock - where $XDG_RUNTIME_DIR is determined using the XDG Base Directory - Specification. - host -- for CommunicationMethod.INET_SOCKET, server hostname - or IP address as a string. If None, the default value is - taken from SPEECHD_HOST environment variable (if it - exists) or from the DEFAULT_HOST attribute of this class. - port -- for CommunicationMethod.INET_SOCKET method, server - port as number or None. If None, the default value is - taken from SPEECHD_PORT environment variable (if it - exists) or from the DEFAULT_PORT attribute of this class. - - For more information on client identification strings see Speech - Dispatcher documentation. - """ - - _home = os.path.expanduser("~") - _runtime_dir = os.environ.get('XDG_RUNTIME_DIR', os.environ.get('XDG_CACHE_HOME', os.path.join(_home, '.cache'))) - _sock_path = os.path.join(_runtime_dir, self.DEFAULT_SOCKET_PATH) - # Resolve connection parameters: - connection_args = {'communication_method': CommunicationMethod.UNIX_SOCKET, - 'socket_path': _sock_path, - 'host': self.DEFAULT_HOST, - 'port': self.DEFAULT_PORT, - } - # Respect address method argument and SPEECHD_ADDRESS environemt variable - _address = address or os.environ.get("SPEECHD_ADDRESS") - - if _address: - connection_args.update(self._connection_arguments_from_address(_address)) - # Respect the old (deprecated) key arguments and environment variables - # TODO: Remove this section in 0.8 release - else: - # Read the environment variables - env_speechd_host = os.environ.get("SPEECHD_HOST") - try: - env_speechd_port = int(os.environ.get("SPEECHD_PORT")) - except: - env_speechd_port = None - env_speechd_socket_path = os.environ.get("SPEECHD_SOCKET") - # Prefer old (deprecated) function arguments, but if - # not specified and old (deprecated) environment variable - # is set, use the value of the environment variable - if method: - connection_args['method'] = method - if port: - connection_args['port'] = port - elif env_speechd_port: - connection_args['port'] = env_speechd_port - if socket_path: - connection_args['socket_path'] = socket_path - elif env_speechd_socket_path: - connection_args['socket_path'] = env_speechd_socket_path - self._connect_with_autospawn(connection_args, autospawn) - self._initialize_connection(user, name, component) - - def _connect_with_autospawn(self, connection_args, autospawn): - """Establish new connection (and/or autospawn server)""" - try: - self._conn = _SSIP_Connection(**connection_args) - except SSIPCommunicationError as ce: - # Suppose server might not be running, try the autospawn mechanism - if autospawn != False: - # Autospawn is however not guaranteed to start the server. The server - # will decide, based on it's configuration, whether to honor the request. - try: - self._server_spawn(connection_args) - except SpawnError as se: - ce.set_additional_exception(se) - raise ce - self._conn = _SSIP_Connection(**connection_args) - else: - raise - - def _initialize_connection(self, user, name, component): - """Initialize connection -- Set client name, get id, register callbacks etc.""" - full_name = '%s:%s:%s' % (user, name, component) - self._conn.send_command('SET', Scope.SELF, 'CLIENT_NAME', full_name) - code, msg, data = self._conn.send_command('HISTORY', 'GET', 'CLIENT_ID') - self._client_id = int(data[0]) - self._callback_handler = _CallbackHandler(self._client_id) - self._conn.set_callback(self._callback_handler) - for event in (CallbackType.INDEX_MARK, - CallbackType.BEGIN, - CallbackType.END, - CallbackType.CANCEL, - CallbackType.PAUSE, - CallbackType.RESUME): - self._conn.send_command('SET', 'self', 'NOTIFICATION', event, 'on') - - def _connection_arguments_from_address(self, address): - """Parse a Speech Dispatcher address line and return a dictionary - of connection arguments""" - connection_args = {} - address_params = address.split(":") - try: - _method = address_params[0] - except: - raise SSIPCommunicationErrror("Wrong format of server address") - connection_args['communication_method'] = _method - if _method == CommunicationMethod.UNIX_SOCKET: - try: - connection_args['socket_path'] = address_params[1] - except IndexError: - pass # The additional parameters was not set, let's stay with defaults - elif _method == CommunicationMethod.INET_SOCKET: - try: - connection_args['host'] = address_params[1] - connection_args['port'] = int(address_params[2]) - except ValueError: # Failed conversion to int - raise SSIPCommunicationError("Third parameter of inet_socket address must be a port number") - except IndexError: - pass # The additional parameters was not set, let's stay with defaults - else: - raise SSIPCommunicationError("Unknown communication method in address."); - return connection_args - - def __del__(self): - """Close the connection""" - self.close() - - def _server_spawn(self, connection_args): - """Attempts to spawn the speech-dispatcher server.""" - # Check whether we are not connecting to a remote host - # TODO: This is a hack. inet sockets specific code should - # belong to _SSIPConnection. We do not however have an _SSIPConnection - # yet. - if connection_args['communication_method'] == 'inet_socket': - addrinfos = socket.getaddrinfo(connection_args['host'], - connection_args['port']) - # Check resolved addrinfos for presence of localhost - ip_addresses = [addrinfo[4][0] for addrinfo in addrinfos] - localhost=False - for ip in ip_addresses: - if ip.startswith("127.") or ip == "::1": - connection_args['host'] = ip - localhost=True - if not localhost: - # The hostname didn't resolve on localhost in neither case, - # do not spawn server on localhost... - raise SpawnError( - "Can't start server automatically (autospawn), requested address %s " - "resolves on %s which seems to be a remote host. You must start the " - "server manually or choose another connection address." % (connection_args['host'], - str(ip_addresses),)) - if os.path.exists(paths.SPD_SPAWN_CMD): - connection_params = [] - for param, value in connection_args.items(): - if param not in ["host",]: - connection_params += ["--"+param.replace("_","-"), str(value)] - - server = subprocess.Popen([paths.SPD_SPAWN_CMD, "--spawn"]+connection_params, - stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout_reply, stderr_reply = server.communicate() - retcode = server.wait() - if retcode != 0: - raise SpawnError("Server refused to autospawn, stating this reason: %s" % (stderr_reply,)) - return server.pid - else: - raise SpawnError("Can't find Speech Dispatcher spawn command %s" - % (paths.SPD_SPAWN_CMD,)) - - def set_priority(self, priority): - """Set the priority category for the following messages. - - Arguments: - priority -- one of the 'Priority' constants. - - """ - assert priority in (Priority.IMPORTANT, Priority.MESSAGE, - Priority.TEXT, Priority.NOTIFICATION, - Priority.PROGRESS), priority - self._conn.send_command('SET', Scope.SELF, 'PRIORITY', priority) - - def set_data_mode(self, value): - """Set the data mode for further speech commands. - - Arguments: - value - one of the constants defined by the DataMode class. - - """ - if value == DataMode.SSML: - ssip_val = 'on' - elif value == DataMode.TEXT: - ssip_val = 'off' - else: - raise ValueError( - 'Value "%s" is not one of the constants from the DataMode class.' % \ - value) - self._conn.send_command('SET', Scope.SELF, 'SSML_MODE', ssip_val) - - def speak(self, text, callback=None, event_types=None): - """Say given message. - - Arguments: - text -- message text to be spoken. This may be either a UTF-8 - encoded byte string or a Python unicode string. - callback -- a callback handler for asynchronous event notifications. - A callable object (function) which accepts one positional argument - `type' and one keyword argument `index_mark'. See below for more - details. - event_types -- a tuple of event types for which the callback should - be called. Each item must be one of `CallbackType' constants. - None (the default value) means to handle all event types. This - argument is irrelevant when `callback' is not used. - - The callback function will be called whenever one of the events occurs. - The event type will be passed as argument. Its value is one of the - `CallbackType' constants. In case of an index mark event, additional - keyword argument `index_mark' will be passed and will contain the index - mark identifier as specified within the text. - - The callback function should not perform anything complicated and is - not allowed to issue any further SSIP client commands. An attempt to - do so would lead to a deadlock in SSIP communication. - - This method is non-blocking; it just sends the command, given - message is queued on the server and the method returns immediately. - - """ - self._conn.send_command('SPEAK') - result = self._conn.send_data(text) - if callback: - msg_id = int(result[2][0]) - # TODO: Here we risk, that the callback arrives earlier, than we - # add the item to `self._callback_handler'. Such a situation will - # lead to the callback being ignored. - self._callback_handler.add_callback(msg_id, callback, event_types) - return result - - def char(self, char): - """Say given character. - - Arguments: - char -- a character to be spoken. Either a Python unicode string or - a UTF-8 encoded byte string. - - This method is non-blocking; it just sends the command, given - message is queued on the server and the method returns immediately. - - """ - self._conn.send_command('CHAR', char.replace(' ', 'space')) - - def key(self, key): - """Say given key name. - - Arguments: - key -- the key name (as defined in SSIP); string. - - This method is non-blocking; it just sends the command, given - message is queued on the server and the method returns immediately. - - """ - self._conn.send_command('KEY', key) - - def sound_icon(self, sound_icon): - """Output given sound_icon. - - Arguments: - sound_icon -- the name of the sound icon as defined by SSIP; string. - - This method is non-blocking; it just sends the command, given message - is queued on the server and the method returns immediately. - - """ - self._conn.send_command('SOUND_ICON', sound_icon) - - def cancel(self, scope=Scope.SELF): - """Immediately stop speaking and discard messages in queues. - - Arguments: - scope -- see the documentation of this class. - - """ - self._conn.send_command('CANCEL', scope) - - - def stop(self, scope=Scope.SELF): - """Immediately stop speaking the currently spoken message. - - Arguments: - scope -- see the documentation of this class. - - """ - self._conn.send_command('STOP', scope) - - def pause(self, scope=Scope.SELF): - """Pause speaking and postpone other messages until resume. - - This method is non-blocking. However, speaking can continue for a - short while even after it's called (typically to the end of the - sentence). - - Arguments: - scope -- see the documentation of this class. - - """ - self._conn.send_command('PAUSE', scope) - - def resume(self, scope=Scope.SELF): - """Resume speaking of the currently paused messages. - - This method is non-blocking. However, speaking can continue for a - short while even after it's called (typically to the end of the - sentence). - - Arguments: - scope -- see the documentation of this class. - - """ - self._conn.send_command('RESUME', scope) - - def list_output_modules(self): - """Return names of all active output modules as a tuple of strings.""" - code, msg, data = self._conn.send_command('LIST', 'OUTPUT_MODULES') - return data - - def list_synthesis_voices(self): - """Return names of all available voices for the current output module. - - Returns a tuple of tripplets (name, language, dialect). - - 'name' is a string, 'language' is an ISO 639-1 Alpha-2 language code - and 'dialect' is a string. Language and dialect may be None. - - """ - try: - code, msg, data = self._conn.send_command('LIST', 'SYNTHESIS_VOICES') - except SSIPCommandError: - return () - def split(item): - name, lang, dialect = tuple(item.rsplit(' ', 3)) - return (name, lang or None, dialect or None) - return tuple([split(item) for item in data]) - - def set_language(self, language, scope=Scope.SELF): - """Switch to a particular language for further speech commands. - - Arguments: - language -- two letter language code according to RFC 1776 as string. - scope -- see the documentation of this class. - - """ - assert isinstance(language, str) and len(language) == 2 - self._conn.send_command('SET', scope, 'LANGUAGE', language) - - def set_output_module(self, name, scope=Scope.SELF): - """Switch to a particular output module. - - Arguments: - name -- module (string) as returned by 'list_output_modules()'. - scope -- see the documentation of this class. - - """ - self._conn.send_command('SET', scope, 'OUTPUT_MODULE', name) - - def set_pitch(self, value, scope=Scope.SELF): - """Set the pitch for further speech commands. - - Arguments: - value -- integer value within the range from -100 to 100, with 0 - corresponding to the default pitch of the current speech synthesis - output module, lower values meaning lower pitch and higher values - meaning higher pitch. - scope -- see the documentation of this class. - - """ - assert isinstance(value, int) and -100 <= value <= 100, value - self._conn.send_command('SET', scope, 'PITCH', value) - - def set_rate(self, value, scope=Scope.SELF): - """Set the speech rate (speed) for further speech commands. - - Arguments: - value -- integer value within the range from -100 to 100, with 0 - corresponding to the default speech rate of the current speech - synthesis output module, lower values meaning slower speech and - higher values meaning faster speech. - scope -- see the documentation of this class. - - """ - assert isinstance(value, int) and -100 <= value <= 100 - self._conn.send_command('SET', scope, 'RATE', value) - - def set_volume(self, value, scope=Scope.SELF): - """Set the speech volume for further speech commands. - - Arguments: - value -- integer value within the range from -100 to 100, with 100 - corresponding to the default speech volume of the current speech - synthesis output module, lower values meaning softer speech. - scope -- see the documentation of this class. - - """ - assert isinstance(value, int) and -100 <= value <= 100 - self._conn.send_command('SET', scope, 'VOLUME', value) - - def set_punctuation(self, value, scope=Scope.SELF): - """Set the punctuation pronounciation level. - - Arguments: - value -- one of the 'PunctuationMode' constants. - scope -- see the documentation of this class. - - """ - assert value in (PunctuationMode.ALL, PunctuationMode.SOME, - PunctuationMode.NONE), value - self._conn.send_command('SET', scope, 'PUNCTUATION', value) - - def set_spelling(self, value, scope=Scope.SELF): - """Toogle the spelling mode or on off. - - Arguments: - value -- if 'True', all incomming messages will be spelled - instead of being read as normal words. 'False' switches - this behavior off. - scope -- see the documentation of this class. - - """ - assert value in [True, False] - if value == True: - self._conn.send_command('SET', scope, 'SPELLING', "on") - else: - self._conn.send_command('SET', scope, 'SPELLING', "off") - - def set_cap_let_recogn(self, value, scope=Scope.SELF): - """Set capital letter recognition mode. - - Arguments: - value -- one of 'none', 'spell', 'icon'. None means no signalization - of capital letters, 'spell' means capital letters will be spelled - with a syntetic voice and 'icon' means that the capital-letter icon - will be prepended before each capital letter. - scope -- see the documentation of this class. - - """ - assert value in ("none", "spell", "icon") - self._conn.send_command('SET', scope, 'CAP_LET_RECOGN', value) - - def set_voice(self, value, scope=Scope.SELF): - """Set voice by a symbolic name. - - Arguments: - value -- one of the SSIP symbolic voice names: 'MALE1' .. 'MALE3', - 'FEMALE1' ... 'FEMALE3', 'CHILD_MALE', 'CHILD_FEMALE' - scope -- see the documentation of this class. - - Symbolic voice names are mapped to real synthesizer voices in the - configuration of the output module. Use the method - 'set_synthesis_voice()' if you want to work with real voices. - - """ - assert isinstance(value, str) and \ - value.lower() in ("male1", "male2", "male3", "female1", - "female2", "female3", "child_male", - "child_female") - self._conn.send_command('SET', scope, 'VOICE', value) - - def set_synthesis_voice(self, value, scope=Scope.SELF): - """Set voice by its real name. - - Arguments: - value -- voice name as returned by 'list_synthesis_voices()' - scope -- see the documentation of this class. - - """ - self._conn.send_command('SET', scope, 'SYNTHESIS_VOICE', value) - - def set_pause_context(self, value, scope=Scope.SELF): - """Set the amount of context when resuming a paused message. - - Arguments: - value -- a positive or negative value meaning how many chunks of data - after or before the pause should be read when resume() is executed. - scope -- see the documentation of this class. - - """ - assert isinstance(value, int) - self._conn.send_command('SET', scope, 'PAUSE_CONTEXT', value) - - def set_debug(self, val): - """Switch debugging on and off. When switched on, - debugging files will be created in the chosen destination - (see set_debug_destination()) for Speech Dispatcher and all - its running modules. All logging information will then be - written into these files with maximal verbosity until switched - off. You should always first call set_debug_destination. - - The intended use of this functionality is to switch debuging - on for a period of time while the user will repeat the behavior - and then send the logs to the appropriate bug-reporting place. - - Arguments: - val -- a boolean value determining whether debugging - is switched on or off - scope -- see the documentation of this class. - - """ - assert isinstance(val, bool) - if val == True: - ssip_val = "ON" - else: - ssip_val = "OFF" - - self._conn.send_command('SET', scope.ALL, 'DEBUG', ssip_val) - - - def set_debug_destination(self, path): - """Set debug destination. - - Arguments: - path -- path (string) to the directory where debuging - files will be created - scope -- see the documentation of this class. - - """ - assert isinstance(val, string) - - self._conn.send_command('SET', scope.ALL, 'DEBUG_DESTINATION', val) - - def block_begin(self): - """Begin an SSIP block. - - See SSIP documentation for more details about blocks. - - """ - self._conn.send_command('BLOCK', 'BEGIN') - - def block_end(self): - """Close an SSIP block. - - See SSIP documentation for more details about blocks. - - """ - self._conn.send_command('BLOCK', 'END') - - def close(self): - """Close the connection to Speech Dispatcher.""" - if hasattr(self, '_conn'): - self._conn.close() - del self._conn - - -class Client(SSIPClient): - """A DEPRECATED backwards-compatible API. - - This Class is provided only for backwards compatibility with the prevoius - unofficial API. It will be removed in future versions. Please use either - 'SSIPClient' or 'Speaker' interface instead. As deprecated, the API is no - longer documented. - - """ - def __init__(self, name=None, client=None, **kwargs): - name = name or client or 'python' - super(Client, self).__init__(name, **kwargs) - - def say(self, text, priority=Priority.MESSAGE): - self.set_priority(priority) - self.speak(text) - - def char(self, char, priority=Priority.TEXT): - self.set_priority(priority) - super(Client, self).char(char) - - def key(self, key, priority=Priority.TEXT): - self.set_priority(priority) - super(Client, self).key(key) - - def sound_icon(self, sound_icon, priority=Priority.TEXT): - self.set_priority(priority) - super(Client, self).sound_icon(sound_icon) - - -class Speaker(SSIPClient): - """Extended Speech Dispatcher Interface. - - This class provides an extended intercace to Speech Dispatcher - functionality and tries to hide most of the lower level details of SSIP - (such as a more sophisticated handling of blocks and priorities and - advanced event notifications) under a more convenient API. - - Please note that the API is not yet stabilized and thus is subject to - change! Please contact the authors if you plan using it and/or if you have - any suggestions. - - Well, in fact this class is currently not implemented at all. It is just a - draft. The intention is to hide the SSIP details and provide a generic - interface practical for screen readers. - - """ - - -# Deprecated but retained for backwards compatibility - -# This class was introduced in 0.7 but later renamed to CommunicationMethod -class ConnectionMethod(object): - """Constants describing the possible methods of connection to server. - - Retained for backwards compatibility but DEPRECATED. See CommunicationMethod.""" - UNIX_SOCKET = 'unix_socket' - """Unix socket communication using a filesystem path""" - INET_SOCKET = 'inet_socket' - """Inet socket communication using a host and port""" diff --git a/src/accessible_output2/outputs/speechd/paths.py b/src/accessible_output2/outputs/speechd/paths.py deleted file mode 100644 index 817bd24..0000000 --- a/src/accessible_output2/outputs/speechd/paths.py +++ /dev/null @@ -1 +0,0 @@ -SPD_SPAWN_CMD = "/usr/bin/speech-dispatcher" diff --git a/src/accessible_output2/outputs/system_access.py b/src/accessible_output2/outputs/system_access.py index cd6099d..f8f3d25 100644 --- a/src/accessible_output2/outputs/system_access.py +++ b/src/accessible_output2/outputs/system_access.py @@ -1,18 +1,24 @@ -from base import Output +from __future__ import absolute_import +import ctypes +from .base import Output class SystemAccess (Output): """Supports System Access and System Access Mobile""" name = "System Access" lib32 = 'saapi32.dll' + argtypes = { + 'SA_BrlShowTextW': (ctypes.c_wchar_p,), + 'SA_SayW': (ctypes.c_wchar_p,), + } priority = 99 def braille(self, text, **options): - self.lib.SA_BrlShowTextW(unicode(text)) + self.lib.SA_BrlShowTextW(text) def speak(self, text, interrupt=False): if self.is_active(): - self.dll.SA_SayW(unicode(text)) + self.dll.SA_SayW(str(text)) def is_active(self): try: diff --git a/src/accessible_output2/outputs/voiceover.py b/src/accessible_output2/outputs/voiceover.py index a9b96c2..6a432e3 100644 --- a/src/accessible_output2/outputs/voiceover.py +++ b/src/accessible_output2/outputs/voiceover.py @@ -1,23 +1 @@ -from base import Output, OutputError - -class VoiceOver (Output): - """Supports the VoiceOver screenreader on the Mac. - - Note that this will also output as a message to the braille display if VoiceOver is used with braille. - Calling this module could cause VoiceOver to be started. - """ - name = 'VoiceOver' - - def __init__(self, *args, **kwargs): - super(VoiceOver, self).__init__(*args, **kwargs) - try: - from appscript import app - self.app = app('VoiceOver') - except ImportError: - raise OutputError - - def speak(self, text, interupt=False): - self.app.output(text) - - def is_active(self): - return True +from __future__ import absolute_import from builtins import str import subprocess, os from .base import Output class VoiceOver(Output): """Speech output supporting the Apple VoiceOver screen reader.""" def runAppleScript(self, command, process = 'voiceover'): return subprocess.Popen(['osascript', '-e', 'tell application "' + process + '"\n' + command + '\nend tell'], stdout = subprocess.PIPE).communicate()[0] name = 'VoiceOver' def speak(self, text, interrupt=0): if interrupt: self.silence() os.system('osascript -e \"tell application \\\"voiceover\\\" to output \\\"%s\\\"\" &' % text) def silence (self): self.runAppleScript('output ""') def is_active(self): return self.runAppleScript('return (name of processes) contains "VoiceOver"', 'system events').startswith('true') and not self.runAppleScript('try\nreturn bounds of vo cursor\non error\nreturn false\nend try').startswith('false') output_class = VoiceOver \ No newline at end of file diff --git a/src/accessible_output2/outputs/window_eyes.py b/src/accessible_output2/outputs/window_eyes.py index 6dd1c31..efed1b6 100644 --- a/src/accessible_output2/outputs/window_eyes.py +++ b/src/accessible_output2/outputs/window_eyes.py @@ -1,6 +1,7 @@ +from __future__ import absolute_import import win32gui from libloader.com import load_com -from base import Output, OutputError +from .base import Output, OutputError import pywintypes class WindowEyes (Output): diff --git a/src/libloader/com.py b/src/libloader/com.py index 75d9ac8..29ede4c 100644 --- a/src/libloader/com.py +++ b/src/libloader/com.py @@ -1,13 +1,26 @@ from pywintypes import com_error +import win32com +import paths +win32com.__gen_path__=paths.data_path(u"com_cache") +import sys +import os +sys.path.append(os.path.join(win32com.__gen_path__, ".")) from win32com.client import gencache +fixed=False def prepare_gencache(): gencache.is_readonly = False gencache.GetGeneratePath() - +def patched_getmodule(modname): + mod=__import__(modname) + return sys.modules[modname] def load_com(*names): + global fixed + if fixed==False: + gencache._GetModule=patched_getmodule + fixed=True result = None for name in names: try: diff --git a/src/libloader/libloader.py b/src/libloader/libloader.py index 6ab2c0e..6aa7c32 100644 --- a/src/libloader/libloader.py +++ b/src/libloader/libloader.py @@ -32,7 +32,7 @@ def load_library(library, x86_path='.', x64_path='.', *args, **kwargs): loaded = _do_load(lib, *args, **kwargs) if loaded is not None: return loaded - raise LibraryLoadError('unable to load %r. Provided library path: %r' % (library, path)) + raise LibraryLoadError('unable to load %r. Provided library path: %r' % (library, lib)) def _do_load(file, *args, **kwargs): loader = TYPES[platform.system()]['loader'] diff --git a/src/output.py b/src/output.py index d7e0912..c655be8 100644 --- a/src/output.py +++ b/src/output.py @@ -8,17 +8,26 @@ import sys speaker = None def speak(text, interrupt=0): - global speaker - if not speaker: - setup() - speaker.speak(text, interrupt) - speaker.braille(text) + global speaker + if not speaker: + setup() + speaker.speak(text, interrupt) + speaker.braille(text) def setup (): - global speaker - logging.debug("Initializing output subsystem.") - try: - speaker = outputs.auto.Auto() - except: - return logging.exception("Output: Error during initialization.") + global speaker + logging.debug("Initializing output subsystem.") + try: +# speaker = speech.Speaker(speech.outputs.Sapi5()) +# else: + speaker = outputs.auto.Auto() + except: + return logging.exception("Output: Error during initialization.") +def copy(text): + import win32clipboard + #Copies text to the clipboard. + win32clipboard.OpenClipboard() + win32clipboard.EmptyClipboard() + win32clipboard.SetClipboardText(text) + win32clipboard.CloseClipboard()