1# Copyright (C) 2003-2008 Brailcom, o.p.s.
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU Lesser General Public License as published by
5# the Free Software Foundation; either version 2.1 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU Lesser General Public License for more details.
12#
13# You should have received a copy of the GNU Lesser General Public License
14# along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16"""Python API to Speech Dispatcher
17
18Basic Python client API to Speech Dispatcher is provided by the 'SSIPClient'
19class.  This interface maps directly to available SSIP commands and logic.
20
21A more convenient interface is provided by the 'Speaker' class.
22
23"""
24
25#TODO: Blocking variants for speak, char, key, sound_icon.
26
27import socket, sys, os, subprocess, time, tempfile
28
29try:
30    import threading
31except:
32    import dummy_threading as threading
33
34from . import paths
35
36class CallbackType(object):
37    """Constants describing the available types of callbacks"""
38    INDEX_MARK = 'index_marks'
39    """Index mark events are reported when the place they were
40    included into the text by the client application is reached
41    when speaking them"""
42    BEGIN = 'begin'
43    """The begin event is reported when Speech Dispatcher starts
44    actually speaking the message."""
45    END = 'end'
46    """The end event is reported after the message has terminated and
47    there is no longer any sound from it being produced"""
48    CANCEL = 'cancel'
49    """The cancel event is reported when a message is canceled either
50    on request of the user, because of prioritization of messages or
51    due to an error"""
52    PAUSE = 'pause'
53    """The pause event is reported after speaking of a message
54    was paused. It no longer produces any audio."""
55    RESUME = 'resume'
56    """The resume event is reported right after speaking of a message
57    was resumed after previous pause."""
58
59class SSIPError(Exception):
60    """Common base class for exceptions during SSIP communication."""
61
62class SSIPCommunicationError(SSIPError):
63    """Exception raised when trying to operate on a closed connection."""
64
65    _additional_exception = None
66
67    def __init__(self, description=None, original_exception=None, **kwargs):
68        self._original_exception = original_exception
69        self._description = description
70        super(SSIPError, self).__init__(**kwargs)
71
72    def original_exception(self):
73        """Return the original exception if any
74
75        If this exception is secondary, being caused by a lower
76        level exception, return this original exception, otherwise
77        None"""
78        return self._original_exception
79
80    def set_additional_exception(self, exception):
81        """Set an additional exception
82
83        See method additional_exception().
84        """
85        self._additional_exception = exception
86
87    def additional_exception(self):
88        """Return an additional exception
89
90        Additional exceptions araise from failed attempts to resolve
91        the former problem"""
92        return self._additional_exception
93
94    def description(self):
95        """Return error description"""
96        return self._description
97
98    def __str__(self):
99        msgs = []
100        if self.description():
101            msgs.append(self.description())
102        if self.original_exception:
103            msgs.append("Original error: " + str(self.original_exception()))
104        if self.additional_exception:
105            msgs.append("Additional error: " + str(self.additional_exception()))
106        return "\n".join(msgs)
107
108class SSIPResponseError(Exception):
109    def __init__(self, code, msg, data):
110        Exception.__init__(self, "%s: %s" % (code, msg))
111        self._code = code
112        self._msg = msg
113        self._data = data
114
115    def code(self):
116        """Return the server response error code as integer number."""
117        return self._code
118
119    def msg(self):
120        """Return server response error message as string."""
121        return self._msg
122
123
124class SSIPCommandError(SSIPResponseError):
125    """Exception raised on error response after sending command."""
126
127    def command(self):
128        """Return the command string which resulted in this error."""
129        return self._data
130
131
132class SSIPDataError(SSIPResponseError):
133    """Exception raised on error response after sending data."""
134
135    def data(self):
136        """Return the data which resulted in this error."""
137        return self._data
138
139
140class SpawnError(Exception):
141    """Indicates failure in server autospawn."""
142
143class CommunicationMethod(object):
144    """Constants describing the possible methods of connection to server."""
145    UNIX_SOCKET = 'unix_socket'
146    """Unix socket communication using a filesystem path"""
147    INET_SOCKET = 'inet_socket'
148    """Inet socket communication using a host and port"""
149
150class _SSIP_Connection(object):
151    """Implemantation of low level SSIP communication."""
152
153    _NEWLINE = b"\r\n"
154    _END_OF_DATA_MARKER = b'.'
155    _END_OF_DATA_MARKER_ESCAPED = b'..'
156    _END_OF_DATA = _NEWLINE + _END_OF_DATA_MARKER + _NEWLINE
157    _END_OF_DATA_ESCAPED = _NEWLINE + _END_OF_DATA_MARKER_ESCAPED + _NEWLINE
158    # Constants representing \r\n. and \r\n..
159    _RAW_DOTLINE = _NEWLINE + _END_OF_DATA_MARKER
160    _ESCAPED_DOTLINE = _NEWLINE + _END_OF_DATA_MARKER_ESCAPED
161
162    _CALLBACK_TYPE_MAP = {700: CallbackType.INDEX_MARK,
163                          701: CallbackType.BEGIN,
164                          702: CallbackType.END,
165                          703: CallbackType.CANCEL,
166                          704: CallbackType.PAUSE,
167                          705: CallbackType.RESUME,
168                          }
169
170    def __init__(self, communication_method, socket_path, host, port):
171        """Init connection: open the socket to server,
172        initialize buffers, launch a communication handling
173        thread.
174        """
175
176        if communication_method == CommunicationMethod.UNIX_SOCKET:
177            socket_family = socket.AF_UNIX
178            socket_connect_args = socket_path
179        elif communication_method == CommunicationMethod.INET_SOCKET:
180            assert host and port
181            socket_family = socket.AF_INET
182            socket_connect_args = (socket.gethostbyname(host), port)
183        else:
184            raise ValueError("Unsupported communication method")
185
186        try:
187            self._socket = socket.socket(socket_family, socket.SOCK_STREAM)
188            self._socket.connect(socket_connect_args)
189        except socket.error as ex:
190            raise SSIPCommunicationError("Can't open socket using method "
191                                         + communication_method,
192                                         original_exception = ex)
193
194        self._buffer = b""
195        self._com_buffer = []
196        self._callback = None
197        self._ssip_reply_semaphore = threading.Semaphore(0)
198        self._communication_thread = \
199                threading.Thread(target=self._communication, kwargs={},
200                                 name="SSIP client communication thread",
201                                 daemon=True)
202        self._communication_thread.start()
203
204    def close(self):
205        """Close the server connection, destroy the communication thread."""
206        # Read-write shutdown here is necessary, otherwise the socket.recv()
207        # function in the other thread won't return at last on some platforms.
208        try:
209            self._socket.shutdown(socket.SHUT_RDWR)
210        except socket.error:
211            pass
212        self._socket.close()
213        # Wait for the other thread to terminate
214        self._communication_thread.join()
215
216    def _communication(self):
217        """Handle incomming socket communication.
218
219        Listens for all incomming communication on the socket, dispatches
220        events and puts all other replies into self._com_buffer list in the
221        already parsed form as (code, msg, data).  Each time a new item is
222        appended to the _com_buffer list, the corresponding semaphore
223        'self._ssip_reply_semaphore' is incremented.
224
225        This method is designed to run in a separate thread.  The thread can be
226        interrupted by closing the socket on which it is listening for
227        reading."""
228
229        while True:
230            try:
231                code, msg, data = self._recv_message()
232            except IOError:
233                # If the socket has been closed, exit the thread
234                sys.exit()
235            if code//100 != 7:
236                # This is not an index mark nor an event
237                self._com_buffer.append((code, msg, data))
238                self._ssip_reply_semaphore.release()
239                continue
240            # Ignore the event if no callback function has been registered.
241            if self._callback is not None:
242                type = self._CALLBACK_TYPE_MAP[code]
243                if type == CallbackType.INDEX_MARK:
244                    kwargs = {'index_mark': data[2]}
245                else:
246                    kwargs = {}
247                # Get message and client ID of the event
248                msg_id, client_id = map(int, data[:2])
249                self._callback(msg_id, client_id, type, **kwargs)
250
251
252    def _readline(self):
253        """Read one whole line from the socket.
254
255        Blocks until the line delimiter ('_NEWLINE') is read.
256
257        """
258        pointer = self._buffer.find(self._NEWLINE)
259        while pointer == -1:
260            try:
261                d = self._socket.recv(1024)
262            except:
263                raise IOError
264            if len(d) == 0:
265                raise IOError
266            self._buffer += d
267            pointer = self._buffer.find(self._NEWLINE)
268        line = self._buffer[:pointer]
269        self._buffer = self._buffer[pointer+len(self._NEWLINE):]
270        return line.decode('utf-8')
271
272    def _recv_message(self):
273        """Read server response or a callback
274        and return the triplet (code, msg, data)."""
275        data = []
276        c = None
277        while True:
278            line = self._readline()
279            assert len(line) >= 4, "Malformed data received from server!"
280            code, sep, text = line[:3], line[3], line[4:]
281            assert code.isalnum() and (c is None or code == c) and \
282                   sep in ('-', ' '), "Malformed data received from server!"
283            if sep == ' ':
284                msg = text
285                return int(code), msg, tuple(data)
286            data.append(text)
287
288    def _recv_response(self):
289        """Read server response from the communication thread
290        and return the triplet (code, msg, data)."""
291        # TODO: This check is dumb but seems to work.  The main thread
292        # hangs without it, when the Speech Dispatcher connection is lost.
293        if not self._communication_thread.is_alive():
294            raise SSIPCommunicationError
295        self._ssip_reply_semaphore.acquire()
296        # The list is sorted, read the first item
297        response = self._com_buffer[0]
298        del self._com_buffer[0]
299        return response
300
301    def send_command(self, command, *args):
302        """Send SSIP command with given arguments and read server response.
303
304        Arguments can be of any data type -- they are all stringified before
305        being sent to the server.
306
307        Returns a triplet (code, msg, data), where 'code' is a numeric SSIP
308        response code as an integer, 'msg' is an SSIP rsponse message as string
309        and 'data' is a tuple of strings (all lines of response data) when a
310        response contains some data.
311
312        'SSIPCommandError' is raised in case of non 2xx return code.  See SSIP
313        documentation for more information about server responses and codes.
314
315        'IOError' is raised when the socket was closed by the remote side.
316
317        """
318        if __debug__:
319            if command in ('SET', 'CANCEL', 'STOP',):
320                assert args[0] in (Scope.SELF, Scope.ALL) \
321                       or isinstance(args[0], int)
322        cmd = ' '.join((command,) + tuple(map(str, args)))
323        try:
324            self._socket.send(cmd.encode('utf-8') + self._NEWLINE)
325        except socket.error:
326            raise SSIPCommunicationError("Speech Dispatcher connection lost.")
327        code, msg, data = self._recv_response()
328        if code//100 != 2:
329            raise SSIPCommandError(code, msg, cmd)
330        return code, msg, data
331
332    def send_data(self, data):
333        """Send multiline data and read server response.
334
335        Returned value is the same as for 'send_command()' method.
336
337        'SSIPDataError' is raised in case of non 2xx return code. See SSIP
338        documentation for more information about server responses and codes.
339
340        'IOError' is raised when the socket was closed by the remote side.
341
342        """
343        data = data.encode('utf-8')
344        # Escape the end-of-data marker even if present at the beginning
345        # The start of the string is also the start of a line.
346        if data.startswith(self._END_OF_DATA_MARKER):
347            l = len(self._END_OF_DATA_MARKER)
348            data = self._END_OF_DATA_MARKER_ESCAPED + data[l:]
349
350        # Escape the end of data marker at the start of each subsequent
351        # line.  We can do that by simply replacing \r\n. with \r\n..,
352        # since the start of a line is immediately preceded by \r\n,
353        # when the line is not the beginning of the string.
354        data = data.replace(self._RAW_DOTLINE, self._ESCAPED_DOTLINE)
355
356        try:
357            self._socket.send(data + self._END_OF_DATA)
358        except socket.error:
359            raise SSIPCommunicationError("Speech Dispatcher connection lost.")
360        code, msg, response_data = self._recv_response()
361        if code//100 != 2:
362            raise SSIPDataError(code, msg, data)
363        return code, msg, response_data
364
365    def set_callback(self, callback):
366        """Register a callback function for handling asynchronous events.
367
368        Arguments:
369          callback -- a callable object (function) which will be called to
370            handle asynchronous events (arguments described below).  Passing
371            `None' results in removing the callback function and ignoring
372            events.  Just one callback may be registered.  Attempts to register
373            a second callback will result in the former callback being
374            replaced.
375
376        The callback function must accept three positional arguments
377        ('message_id', 'client_id', 'event_type') and an optional keyword
378        argument 'index_mark' (when INDEX_MARK events are turned on).
379
380        Note, that setting the callback function doesn't turn the events on.
381        The user is responsible to turn them on by sending the appropriate `SET
382        NOTIFICATION' command.
383
384        """
385        self._callback = callback
386
387class _CallbackHandler(object):
388    """Internal object which handles callbacks."""
389
390    def __init__(self, client_id):
391        self._client_id = client_id
392        self._callbacks = {}
393        self._lock = threading.Lock()
394
395    def __call__(self, msg_id, client_id, type, **kwargs):
396        if client_id != self._client_id:
397            # TODO: does that ever happen?
398            return
399        self._lock.acquire()
400        try:
401            try:
402                callback, event_types = self._callbacks[msg_id]
403            except KeyError:
404                pass
405            else:
406                if event_types is None or type in event_types:
407                    callback(type, **kwargs)
408                if type in (CallbackType.END, CallbackType.CANCEL):
409                    del self._callbacks[msg_id]
410        finally:
411            self._lock.release()
412
413    def add_callback(self, msg_id,  callback, event_types):
414        self._lock.acquire()
415        try:
416            self._callbacks[msg_id] = (callback, event_types)
417        finally:
418            self._lock.release()
419
420class Scope(object):
421    """An enumeration of valid SSIP command scopes.
422
423    The constants of this class should be used to specify the 'scope' argument
424    for the 'Client' methods.
425
426    """
427    SELF = 'self'
428    """The command (mostly a setting) applies to current connection only."""
429    ALL = 'all'
430    """The command applies to all current Speech Dispatcher connections."""
431
432
433class Priority(object):
434    """An enumeration of valid SSIP message priorities.
435
436    The constants of this class should be used to specify the 'priority'
437    argument for the 'Client' methods.  For more information about message
438    priorities and their interaction, see the SSIP documentation.
439
440    """
441    IMPORTANT = 'important'
442    TEXT = 'text'
443    MESSAGE = 'message'
444    NOTIFICATION = 'notification'
445    PROGRESS = 'progress'
446
447
448class PunctuationMode(object):
449    """Constants for selecting a punctuation mode.
450
451    The mode determines which characters should be read.
452
453    """
454    ALL = 'all'
455    """Read all punctuation characters."""
456    NONE = 'none'
457    """Don't read any punctuation character at all."""
458    SOME = 'some'
459    """Only some of the user-defined punctuation characters are read."""
460    MOST = 'most'
461    """Only most of the user-defined punctuation characters are read.
462
463    The set of characters is specified in Speech Dispatcher configuration.
464
465    """
466
467class DataMode(object):
468    """Constants specifying the type of data contained within messages
469    to be spoken.
470
471    """
472    TEXT = 'text'
473    """Data is plain text."""
474    SSML = 'ssml'
475    """Data is SSML (Speech Synthesis Markup Language)."""
476
477
478class SSIPClient(object):
479    """Basic Speech Dispatcher client interface.
480
481    This class provides a Python interface to Speech Dispatcher functionality
482    over an SSIP connection.  The API maps directly to available SSIP commands.
483    Each connection to Speech Dispatcher is represented by one instance of this
484    class.
485
486    Many commands take the 'scope' argument, thus it is shortly documented
487    here.  It is either one of 'Scope' constants or a number of connection.  By
488    specifying the connection number, you are applying the command to a
489    particular connection.  This feature is only meant to be used by Speech
490    Dispatcher control application, however.  More datails can be found in
491    Speech Dispatcher documentation.
492
493    """
494
495    DEFAULT_HOST = '127.0.0.1'
496    """Default host for server connections."""
497    DEFAULT_PORT = 6560
498    """Default port number for server connections."""
499    DEFAULT_SOCKET_PATH = "speech-dispatcher/speechd.sock"
500    """Default name of the communication unix socket"""
501
502    def __init__(self, name, component='default', user='unknown', address=None,
503                 autospawn=None,
504                 # Deprecated ->
505                 host=None, port=None, method=None, socket_path=None):
506        """Initialize the instance and connect to the server.
507
508        Arguments:
509          name -- client identification string
510          component -- connection identification string.  When one client opens
511            multiple connections, this can be used to identify each of them.
512          user -- user identification string (user name).  When multi-user
513            acces is expected, this can be used to identify their connections.
514          address -- server address as specified in Speech Dispatcher
515            documentation (e.g. "unix:/run/user/joe/speech-dispatcher/speechd.sock"
516            or "inet:192.168.0.85:6561")
517          autospawn -- a flag to specify whether the library should
518            try to start the server if it determines its not already
519            running or not
520
521        Deprecated arguments:
522          method -- communication method to use, one of the constants defined in class
523            CommunicationMethod
524          socket_path -- for CommunicationMethod.UNIX_SOCKET, socket
525            path in filesystem. By default, this is $XDG_RUNTIME_DIR/speech-dispatcher/speechd.sock
526            where $XDG_RUNTIME_DIR is determined using the XDG Base Directory
527            Specification.
528          host -- for CommunicationMethod.INET_SOCKET, server hostname
529            or IP address as a string.  If None, the default value is
530            taken from SPEECHD_HOST environment variable (if it
531            exists) or from the DEFAULT_HOST attribute of this class.
532          port -- for CommunicationMethod.INET_SOCKET method, server
533            port as number or None.  If None, the default value is
534            taken from SPEECHD_PORT environment variable (if it
535            exists) or from the DEFAULT_PORT attribute of this class.
536
537        For more information on client identification strings see Speech
538        Dispatcher documentation.
539        """
540
541        _home = os.path.expanduser("~")
542        _runtime_dir = os.environ.get('XDG_RUNTIME_DIR', os.environ.get('XDG_CACHE_HOME', os.path.join(_home, '.cache')))
543        _sock_path = os.path.join(_runtime_dir, self.DEFAULT_SOCKET_PATH)
544        # Resolve connection parameters:
545        connection_args = {'communication_method': CommunicationMethod.UNIX_SOCKET,
546                           'socket_path': _sock_path,
547                           'host': self.DEFAULT_HOST,
548                           'port': self.DEFAULT_PORT,
549                           }
550        # Respect address method argument and SPEECHD_ADDRESS environemt variable
551        _address = address or os.environ.get("SPEECHD_ADDRESS")
552
553        if _address:
554            connection_args.update(self._connection_arguments_from_address(_address))
555        # Respect the old (deprecated) key arguments and environment variables
556        # TODO: Remove this section in 0.8 release
557        else:
558            # Read the environment variables
559            env_speechd_host = os.environ.get("SPEECHD_HOST")
560            try:
561                env_speechd_port = int(os.environ.get("SPEECHD_PORT"))
562            except:
563                env_speechd_port = None
564            env_speechd_socket_path = os.environ.get("SPEECHD_SOCKET")
565            # Prefer old (deprecated) function arguments, but if
566            # not specified and old (deprecated) environment variable
567            # is set, use the value of the environment variable
568            if method:
569                connection_args['method'] = method
570            if port:
571                connection_args['port'] = port
572            elif env_speechd_port:
573                connection_args['port'] = env_speechd_port
574            if socket_path:
575                connection_args['socket_path'] = socket_path
576            elif env_speechd_socket_path:
577                connection_args['socket_path'] = env_speechd_socket_path
578        self._connect_with_autospawn(connection_args, autospawn)
579        self._initialize_connection(user, name, component)
580
581    def _connect_with_autospawn(self, connection_args, autospawn):
582        """Establish new connection (and/or autospawn server)"""
583        try:
584            self._conn = _SSIP_Connection(**connection_args)
585        except SSIPCommunicationError as ce:
586            # Suppose server might not be running, try the autospawn mechanism
587            if autospawn != False:
588                # Autospawn is however not guaranteed to start the server. The server
589                # will decide, based on it's configuration, whether to honor the request.
590                try:
591                    self._server_spawn(connection_args)
592                except SpawnError as se:
593                    ce.set_additional_exception(se)
594                    raise ce
595                self._conn = _SSIP_Connection(**connection_args)
596            else:
597                raise
598
599    def _initialize_connection(self, user, name, component):
600        """Initialize connection -- Set client name, get id, register callbacks etc."""
601        full_name = '%s:%s:%s' % (user, name, component)
602        self._conn.send_command('SET', Scope.SELF, 'CLIENT_NAME', full_name)
603        code, msg, data = self._conn.send_command('HISTORY', 'GET', 'CLIENT_ID')
604        self._client_id = int(data[0])
605        self._callback_handler = _CallbackHandler(self._client_id)
606        self._conn.set_callback(self._callback_handler)
607        for event in (CallbackType.INDEX_MARK,
608                      CallbackType.BEGIN,
609                      CallbackType.END,
610                      CallbackType.CANCEL,
611                      CallbackType.PAUSE,
612                      CallbackType.RESUME):
613            self._conn.send_command('SET', 'self', 'NOTIFICATION', event, 'on')
614
615    def _connection_arguments_from_address(self, address):
616        """Parse a Speech Dispatcher address line and return a dictionary
617        of connection arguments"""
618        connection_args = {}
619        address_params = address.split(":")
620        try:
621            _method = address_params[0]
622        except:
623            raise SSIPCommunicationErrror("Wrong format of server address")
624        connection_args['communication_method'] = _method
625        if _method == CommunicationMethod.UNIX_SOCKET:
626            try:
627                connection_args['socket_path'] = address_params[1]
628            except IndexError:
629                pass # The additional parameters was not set, let's stay with defaults
630        elif _method == CommunicationMethod.INET_SOCKET:
631            try:
632                connection_args['host'] = address_params[1]
633                connection_args['port'] = int(address_params[2])
634            except ValueError: # Failed conversion to int
635                raise SSIPCommunicationError("Third parameter of inet_socket address must be a port number")
636            except IndexError:
637                pass # The additional parameters was not set, let's stay with defaults
638        else:
639            raise SSIPCommunicationError("Unknown communication method in address.");
640        return connection_args
641
642    def __del__(self):
643        """Close the connection"""
644        self.close()
645
646    def _server_spawn(self, connection_args):
647        """Attempts to spawn the speech-dispatcher server."""
648        # Check whether we are not connecting to a remote host
649        # TODO: This is a hack. inet sockets specific code should
650        # belong to _SSIPConnection. We do not however have an _SSIPConnection
651        # yet.
652        if connection_args['communication_method'] == 'inet_socket':
653            addrinfos = socket.getaddrinfo(connection_args['host'],
654                                           connection_args['port'])
655            # Check resolved addrinfos for presence of localhost
656            ip_addresses = [addrinfo[4][0] for addrinfo in addrinfos]
657            localhost=False
658            for ip in ip_addresses:
659                if ip.startswith("127.") or ip == "::1":
660                    connection_args['host'] = ip
661                    localhost=True
662            if not localhost:
663                # The hostname didn't resolve on localhost in neither case,
664                # do not spawn server on localhost...
665                raise SpawnError(
666                    "Can't start server automatically (autospawn), requested address %s "
667                    "resolves on %s which seems to be a remote host. You must start the "
668                    "server manually or choose another connection address." % (connection_args['host'],
669                                                                               str(ip_addresses),))
670        if os.path.exists(paths.SPD_SPAWN_CMD):
671            connection_params = []
672            for param, value in connection_args.items():
673                if param not in ["host",]:
674                    connection_params += ["--"+param.replace("_","-"), str(value)]
675
676            server = subprocess.Popen([paths.SPD_SPAWN_CMD, "--spawn"]+connection_params,
677                                      stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
678            stdout_reply, stderr_reply = server.communicate()
679            retcode = server.wait()
680            if retcode != 0:
681                raise SpawnError("Server refused to autospawn, stating this reason: %s" % (stderr_reply,))
682            return server.pid
683        else:
684            raise SpawnError("Can't find Speech Dispatcher spawn command %s"
685                                         % (paths.SPD_SPAWN_CMD,))
686
687    def set_priority(self, priority):
688        """Set the priority category for the following messages.
689
690        Arguments:
691          priority -- one of the 'Priority' constants.
692
693        """
694        assert priority in (Priority.IMPORTANT, Priority.MESSAGE,
695                            Priority.TEXT, Priority.NOTIFICATION,
696                            Priority.PROGRESS), priority
697        self._conn.send_command('SET', Scope.SELF, 'PRIORITY', priority)
698
699    def set_data_mode(self, value):
700        """Set the data mode for further speech commands.
701
702        Arguments:
703          value - one of the constants defined by the DataMode class.
704
705        """
706        if value == DataMode.SSML:
707            ssip_val = 'on'
708        elif value == DataMode.TEXT:
709            ssip_val = 'off'
710        else:
711            raise ValueError(
712                'Value "%s" is not one of the constants from the DataMode class.' % \
713                    value)
714        self._conn.send_command('SET', Scope.SELF, 'SSML_MODE', ssip_val)
715
716    def speak(self, text, callback=None, event_types=None):
717        """Say given message.
718
719        Arguments:
720          text -- message text to be spoken.  This may be either a UTF-8
721            encoded byte string or a Python unicode string.
722          callback -- a callback handler for asynchronous event notifications.
723            A callable object (function) which accepts one positional argument
724            `type' and one keyword argument `index_mark'.  See below for more
725            details.
726          event_types -- a tuple of event types for which the callback should
727            be called.  Each item must be one of `CallbackType' constants.
728            None (the default value) means to handle all event types.  This
729            argument is irrelevant when `callback' is not used.
730
731        The callback function will be called whenever one of the events occurs.
732        The event type will be passed as argument.  Its value is one of the
733        `CallbackType' constants.  In case of an index mark event, additional
734        keyword argument `index_mark' will be passed and will contain the index
735        mark identifier as specified within the text.
736
737        The callback function should not perform anything complicated and is
738        not allowed to issue any further SSIP client commands.  An attempt to
739        do so would lead to a deadlock in SSIP communication.
740
741        This method is non-blocking;  it just sends the command, given
742        message is queued on the server and the method returns immediately.
743
744        """
745        self._conn.send_command('SPEAK')
746        result = self._conn.send_data(text)
747        if callback:
748            msg_id = int(result[2][0])
749            # TODO: Here we risk, that the callback arrives earlier, than we
750            # add the item to `self._callback_handler'.  Such a situation will
751            # lead to the callback being ignored.
752            self._callback_handler.add_callback(msg_id, callback, event_types)
753        return result
754
755    def char(self, char):
756        """Say given character.
757
758        Arguments:
759          char -- a character to be spoken.  Either a Python unicode string or
760            a UTF-8 encoded byte string.
761
762        This method is non-blocking;  it just sends the command, given
763        message is queued on the server and the method returns immediately.
764
765        """
766        self._conn.send_command('CHAR', char.replace(' ', 'space'))
767
768    def key(self, key):
769        """Say given key name.
770
771        Arguments:
772          key -- the key name (as defined in SSIP); string.
773
774        This method is non-blocking;  it just sends the command, given
775        message is queued on the server and the method returns immediately.
776
777        """
778        self._conn.send_command('KEY', key)
779
780    def sound_icon(self, sound_icon):
781        """Output given sound_icon.
782
783        Arguments:
784          sound_icon -- the name of the sound icon as defined by SSIP; string.
785
786        This method is non-blocking; it just sends the command, given message
787        is queued on the server and the method returns immediately.
788
789        """
790        self._conn.send_command('SOUND_ICON', sound_icon)
791
792    def cancel(self, scope=Scope.SELF):
793        """Immediately stop speaking and discard messages in queues.
794
795        Arguments:
796          scope -- see the documentation of this class.
797
798        """
799        self._conn.send_command('CANCEL', scope)
800
801
802    def stop(self, scope=Scope.SELF):
803        """Immediately stop speaking the currently spoken message.
804
805        Arguments:
806          scope -- see the documentation of this class.
807
808        """
809        self._conn.send_command('STOP', scope)
810
811    def pause(self, scope=Scope.SELF):
812        """Pause speaking and postpone other messages until resume.
813
814        This method is non-blocking.  However, speaking can continue for a
815        short while even after it's called (typically to the end of the
816        sentence).
817
818        Arguments:
819          scope -- see the documentation of this class.
820
821        """
822        self._conn.send_command('PAUSE', scope)
823
824    def resume(self, scope=Scope.SELF):
825        """Resume speaking of the currently paused messages.
826
827        This method is non-blocking.  However, speaking can continue for a
828        short while even after it's called (typically to the end of the
829        sentence).
830
831        Arguments:
832          scope -- see the documentation of this class.
833
834        """
835        self._conn.send_command('RESUME', scope)
836
837    def list_output_modules(self):
838        """Return names of all active output modules as a tuple of strings."""
839        code, msg, data = self._conn.send_command('LIST', 'OUTPUT_MODULES')
840        return data
841
842    def list_synthesis_voices(self):
843        """Return names of all available voices for the current output module.
844
845        Returns a tuple of tripplets (name, language, variant).
846
847        'name' is a string, 'language' is an ISO 639-1 Alpha-2/3 language code
848        and 'variant' is a string.  Language and variant may be None.
849
850        """
851        try:
852            code, msg, data = self._conn.send_command('LIST', 'SYNTHESIS_VOICES')
853        except SSIPCommandError:
854            return ()
855        def split(item):
856            name, lang, variant = tuple(item.rsplit('\t', 3))
857            return (name, lang or None, variant or None)
858        return tuple([split(item) for item in data])
859
860    def set_language(self, language, scope=Scope.SELF):
861        """Switch to a particular language for further speech commands.
862
863        Arguments:
864          language -- two/three letter language code according to RFC 1766 as string, possibly with a region qualification.
865          scope -- see the documentation of this class.
866
867        """
868        assert isinstance(language, str)
869        self._conn.send_command('SET', scope, 'LANGUAGE', language)
870
871    def get_language(self):
872        """Get the current language."""
873        code, msg, data = self._conn.send_command('GET', 'LANGUAGE')
874        if data:
875            return data[0]
876        return None
877
878    def set_output_module(self, name, scope=Scope.SELF):
879        """Switch to a particular output module.
880
881        Arguments:
882          name -- module (string) as returned by 'list_output_modules()'.
883          scope -- see the documentation of this class.
884
885        """
886        self._conn.send_command('SET', scope, 'OUTPUT_MODULE', name)
887
888    def get_output_module(self):
889        """Get the current output module."""
890        code, msg, data = self._conn.send_command('GET', 'OUTPUT_MODULE')
891        if data:
892            return data[0]
893        return None
894
895    def set_pitch(self, value, scope=Scope.SELF):
896        """Set the pitch for further speech commands.
897
898        Arguments:
899          value -- integer value within the range from -100 to 100, with 0
900            corresponding to the default pitch of the current speech synthesis
901            output module, lower values meaning lower pitch and higher values
902            meaning higher pitch.
903          scope -- see the documentation of this class.
904
905        """
906        assert isinstance(value, int) and -100 <= value <= 100, value
907        self._conn.send_command('SET', scope, 'PITCH', value)
908
909    def get_pitch(self):
910        """Get the current pitch."""
911        code, msg, data = self._conn.send_command('GET', 'PITCH')
912        if data:
913            return data[0]
914        return None
915
916    def set_pitch_range(self, value, scope=Scope.SELF):
917        """Set the pitch range for further speech commands.
918
919        Arguments:
920          value -- integer value within the range from -100 to 100, with 0
921            corresponding to the default pitch range of the current speech synthesis
922            output module, lower values meaning lower pitch range and higher values
923            meaning higher pitch range.
924          scope -- see the documentation of this class.
925
926        """
927        assert isinstance(value, int) and -100 <= value <= 100, value
928        self._conn.send_command('SET', scope, 'PITCH_RANGE', value)
929
930    def set_rate(self, value, scope=Scope.SELF):
931        """Set the speech rate (speed) for further speech commands.
932
933        Arguments:
934          value -- integer value within the range from -100 to 100, with 0
935            corresponding to the default speech rate of the current speech
936            synthesis output module, lower values meaning slower speech and
937            higher values meaning faster speech.
938          scope -- see the documentation of this class.
939
940        """
941        assert isinstance(value, int) and -100 <= value <= 100
942        self._conn.send_command('SET', scope, 'RATE', value)
943
944    def get_rate(self):
945        """Get the current speech rate (speed)."""
946        code, msg, data = self._conn.send_command('GET', 'RATE')
947        if data:
948            return data[0]
949        return None
950
951    def set_volume(self, value, scope=Scope.SELF):
952        """Set the speech volume for further speech commands.
953
954        Arguments:
955          value -- integer value within the range from -100 to 100, with 100
956            corresponding to the default speech volume of the current speech
957            synthesis output module, lower values meaning softer speech.
958          scope -- see the documentation of this class.
959
960        """
961        assert isinstance(value, int) and -100 <= value <= 100
962        self._conn.send_command('SET', scope, 'VOLUME', value)
963
964    def get_volume(self):
965        """Get the speech volume."""
966        code, msg, data = self._conn.send_command('GET', 'VOLUME')
967        if data:
968            return data[0]
969        return None
970
971    def set_punctuation(self, value, scope=Scope.SELF):
972        """Set the punctuation pronounciation level.
973
974        Arguments:
975          value -- one of the 'PunctuationMode' constants.
976          scope -- see the documentation of this class.
977
978        """
979        assert value in (PunctuationMode.ALL, PunctuationMode.MOST,
980                         PunctuationMode.SOME, PunctuationMode.NONE), value
981        self._conn.send_command('SET', scope, 'PUNCTUATION', value)
982
983    def get_punctuation(self):
984        """Get the punctuation pronounciation level."""
985        code, msg, data = self._conn.send_command('GET', 'PUNCTUATION')
986        if data:
987            return data[0]
988        return None
989
990    def set_spelling(self, value, scope=Scope.SELF):
991        """Toogle the spelling mode or on off.
992
993        Arguments:
994          value -- if 'True', all incomming messages will be spelled
995            instead of being read as normal words. 'False' switches
996            this behavior off.
997          scope -- see the documentation of this class.
998
999        """
1000        assert value in [True, False]
1001        if value == True:
1002            self._conn.send_command('SET', scope, 'SPELLING', "on")
1003        else:
1004            self._conn.send_command('SET', scope, 'SPELLING', "off")
1005
1006    def set_cap_let_recogn(self, value, scope=Scope.SELF):
1007        """Set capital letter recognition mode.
1008
1009        Arguments:
1010          value -- one of 'none', 'spell', 'icon'. None means no signalization
1011            of capital letters, 'spell' means capital letters will be spelled
1012            with a syntetic voice and 'icon' means that the capital-letter icon
1013            will be prepended before each capital letter.
1014          scope -- see the documentation of this class.
1015
1016        """
1017        assert value in ("none", "spell", "icon")
1018        self._conn.send_command('SET', scope, 'CAP_LET_RECOGN', value)
1019
1020    def set_voice(self, value, scope=Scope.SELF):
1021        """Set voice by a symbolic name.
1022
1023        Arguments:
1024          value -- one of the SSIP symbolic voice names: 'MALE1' .. 'MALE3',
1025            'FEMALE1' ... 'FEMALE3', 'CHILD_MALE', 'CHILD_FEMALE'
1026          scope -- see the documentation of this class.
1027
1028        Symbolic voice names are mapped to real synthesizer voices in the
1029        configuration of the output module.  Use the method
1030        'set_synthesis_voice()' if you want to work with real voices.
1031
1032        """
1033        assert isinstance(value, str) and \
1034               value.lower() in ("male1", "male2", "male3", "female1",
1035                                 "female2", "female3", "child_male",
1036                                 "child_female")
1037        self._conn.send_command('SET', scope, 'VOICE_TYPE', value)
1038
1039    def set_synthesis_voice(self, value, scope=Scope.SELF):
1040        """Set voice by its real name.
1041
1042        Arguments:
1043          value -- voice name as returned by 'list_synthesis_voices()'
1044          scope -- see the documentation of this class.
1045
1046        """
1047        self._conn.send_command('SET', scope, 'SYNTHESIS_VOICE', value)
1048
1049    def set_pause_context(self, value, scope=Scope.SELF):
1050        """Set the amount of context when resuming a paused message.
1051
1052        Arguments:
1053          value -- a positive or negative value meaning how many chunks of data
1054            after or before the pause should be read when resume() is executed.
1055          scope -- see the documentation of this class.
1056
1057        """
1058        assert isinstance(value, int)
1059        self._conn.send_command('SET', scope, 'PAUSE_CONTEXT', value)
1060
1061    def set_debug(self, val):
1062        """Switch debugging on and off. When switched on,
1063        debugging files will be created in the chosen destination
1064        (see set_debug_destination()) for Speech Dispatcher and all
1065        its running modules. All logging information will then be
1066        written into these files with maximal verbosity until switched
1067        off. You should always first call set_debug_destination.
1068
1069        The intended use of this functionality is to switch debuging
1070        on for a period of time while the user will repeat the behavior
1071        and then send the logs to the appropriate bug-reporting place.
1072
1073        Arguments:
1074          val -- a boolean value determining whether debugging
1075                 is switched on or off
1076          scope -- see the documentation of this class.
1077
1078        """
1079        assert isinstance(val, bool)
1080        if val == True:
1081            ssip_val = "ON"
1082        else:
1083            ssip_val = "OFF"
1084
1085        self._conn.send_command('SET', scope.ALL, 'DEBUG', ssip_val)
1086
1087
1088    def set_debug_destination(self, path):
1089        """Set debug destination.
1090
1091        Arguments:
1092          path -- path (string) to the directory where debuging
1093                  files will be created
1094          scope -- see the documentation of this class.
1095
1096        """
1097        assert isinstance(val, string)
1098
1099        self._conn.send_command('SET', scope.ALL, 'DEBUG_DESTINATION', val)
1100
1101    def block_begin(self):
1102        """Begin an SSIP block.
1103
1104        See SSIP documentation for more details about blocks.
1105
1106        """
1107        self._conn.send_command('BLOCK', 'BEGIN')
1108
1109    def block_end(self):
1110        """Close an SSIP block.
1111
1112        See SSIP documentation for more details about blocks.
1113
1114        """
1115        self._conn.send_command('BLOCK', 'END')
1116
1117    def close(self):
1118        """Close the connection to Speech Dispatcher."""
1119        if hasattr(self, '_conn'):
1120            self._conn.close()
1121            del self._conn
1122
1123
1124class Client(SSIPClient):
1125    """A DEPRECATED backwards-compatible API.
1126
1127    This Class is provided only for backwards compatibility with the prevoius
1128    unofficial API.  It will be removed in future versions.  Please use either
1129    'SSIPClient' or 'Speaker' interface instead.  As deprecated, the API is no
1130    longer documented.
1131
1132    """
1133    def __init__(self, name=None, client=None, **kwargs):
1134        name = name or client or 'python'
1135        super(Client, self).__init__(name, **kwargs)
1136
1137    def say(self, text, priority=Priority.MESSAGE):
1138        self.set_priority(priority)
1139        self.speak(text)
1140
1141    def char(self, char, priority=Priority.TEXT):
1142        self.set_priority(priority)
1143        super(Client, self).char(char)
1144
1145    def key(self, key, priority=Priority.TEXT):
1146        self.set_priority(priority)
1147        super(Client, self).key(key)
1148
1149    def sound_icon(self, sound_icon, priority=Priority.TEXT):
1150        self.set_priority(priority)
1151        super(Client, self).sound_icon(sound_icon)
1152
1153
1154class Speaker(SSIPClient):
1155    """Extended Speech Dispatcher Interface.
1156
1157    This class provides an extended intercace to Speech Dispatcher
1158    functionality and tries to hide most of the lower level details of SSIP
1159    (such as a more sophisticated handling of blocks and priorities and
1160    advanced event notifications) under a more convenient API.
1161
1162    Please note that the API is not yet stabilized and thus is subject to
1163    change!  Please contact the authors if you plan using it and/or if you have
1164    any suggestions.
1165
1166    Well, in fact this class is currently not implemented at all.  It is just a
1167    draft.  The intention is to hide the SSIP details and provide a generic
1168    interface practical for screen readers.
1169
1170    """
1171
1172
1173# Deprecated but retained for backwards compatibility
1174
1175# This class was introduced in 0.7 but later renamed to CommunicationMethod
1176class ConnectionMethod(object):
1177    """Constants describing the possible methods of connection to server.
1178
1179    Retained for backwards compatibility but DEPRECATED. See CommunicationMethod."""
1180    UNIX_SOCKET = 'unix_socket'
1181    """Unix socket communication using a filesystem path"""
1182    INET_SOCKET = 'inet_socket'
1183    """Inet socket communication using a host and port"""
1184