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