Package speakeasy :: Module speakeasy_controller
[hide private]
[frames] | no frames]

Source Code for Module speakeasy.speakeasy_controller

   1  #!/usr/bin/python 
   2   
   3  # TODO: 
   4  #   - Robot ops with new msg 
   5  #   - Switch live between local and remote ops.  
   6   
   7  # Note: Unfortunately, this code was written before I knew about QtCreator. So  
   8  #       all of the UI is created in code (see file speakeasy_ui.py). 
   9   
  10  import roslib; roslib.load_manifest('speakeasy'); 
  11   
  12  import sys 
  13  import os 
  14  import signal 
  15  import socket 
  16  import time 
  17  import subprocess 
  18  import re 
  19  import shutil 
  20  import threading 
  21  from functools import partial; 
  22  from threading import Timer; 
  23   
  24  from python_qt_binding.QtGui import QApplication, QMessageBox, QPushButton; 
  25  from python_qt_binding.QtCore import QSocketNotifier, QTimer, Slot; 
  26   
  27   
  28  from utilities.speakeasy_utils import SpeakeasyUtils;  
  29   
  30  from sound_player import SoundPlayer; 
  31  from text_to_speech import TextToSpeechProvider; 
  32   
  33  from robot_interaction import RoboComm; 
  34   
  35  from speakeasy_ui import SpeakEasyGUI; 
  36  from speakeasy_ui import DialogService; 
  37  from speakeasy_ui import PlayLocation; 
  38  from speakeasy_ui import DEFAULT_PLAY_LOCATION; 
  39   
  40  from speakeasy_ui import alternateLookHandler; 
  41  from speakeasy_ui import standardLookHandler; 
  42   
  43  from speakeasy.speakeasy_persistence import ButtonSavior; 
  44   
  45  from speakeasy.buttonSetPopupSelector_ui import ButtonSetPopupSelector; 
  46  from speakeasy import speakeasy_persistence 
47 48 #TODO: Delete: 49 ## Try importing ROS related modules. Remember whether 50 ## that worked. In the SpeakEasyController __init__() method 51 ## we'll switch to local, and warn user if appropriate: 52 #try: 53 # import roslib; roslib.load_manifest('speakeasy'); 54 # import rospy 55 # ROS_IMPORT_OK = True; 56 #except ImportError: 57 # # Ros not installed on this machine; run locally: 58 # ROS_IMPORT_OK = False; 59 # ----------------------------------------------- Class Program ------------------------------------ 60 61 -class ButtonProgram(object):
62 63 #---------------------------------- 64 # Initializer 65 #-------------- 66
67 - def __init__(self, buttonLabel, textToSave, voice, ttsEngine, playOnce=True):
68 ''' 69 Create an object that holds the playback parameters for a 70 programmed button. This initializer is used in two contexts. When the ui 71 is first built, and when a button program set is reconstituted from an 72 XML file. 73 74 @param buttonLabel: The label on the button 75 @type buttonLabel: string 76 @param textToSave: Text to play back with this button. 77 @type textToSave: string 78 @param voice: The voice to use for the utterance 79 @type voice: string 80 @param ttsEngine: The text-to-speech engine to use. (e.g. "festival", or "cepstral" 81 @type ttsEngine: string 82 @param playOnce: Whether to play the utterance just once, or several times. 83 @type playOnce: bool 84 ''' 85 self.buttonLabel = buttonLabel; 86 self.textToSay = textToSave; 87 self.activeVoice = voice; 88 self.ttsEngine = ttsEngine; 89 self.playOnce = playOnce;
90 91 #---------------------------------- 92 # getText 93 #-------------- 94 95
96 - def getText(self):
97 return self.textToSay;
98 99 #---------------------------------- 100 # setText 101 #-------------- 102
103 - def setText(self, newText):
104 ''' 105 Change the button program's utterance text to newText. 106 @param newText: The new utterance. 107 @type newText: string 108 ''' 109 self.textToSay = newText;
110 111 #---------------------------------- 112 # getLabel 113 #-------------- 114
115 - def getLabel(self):
116 return self.buttonLabel;
117 118 #---------------------------------- 119 # getTtsEngine 120 #-------------- 121
122 - def getTtsEngine(self):
123 return self.ttsEngine;
124 125 #---------------------------------- 126 # getVoice 127 #-------------- 128
129 - def getVoice(self):
130 return self.activeVoice;
131 132 #---------------------------------- 133 # toXML 134 #-------------- 135
136 - def toXML(self):
137 138 domOneButtonProgramRoot = ButtonSavior.createXMLElement(ButtonSavior.BUTTON_PROGRAM_TAG); 139 140 domOneButtonProgramRoot.append(ButtonSavior.createXMLElement(ButtonSavior.BUTTON_LABEL_TAG, content=self.buttonLabel)); 141 domOneButtonProgramRoot.append(ButtonSavior.createXMLElement(ButtonSavior.BUTTON_TEXT_TO_SAY_TAG, content=self.textToSay)); 142 domOneButtonProgramRoot.append(ButtonSavior.createXMLElement(ButtonSavior.BUTTON_VOICE_TAG, content=self.activeVoice)); 143 domOneButtonProgramRoot.append(ButtonSavior.createXMLElement(ButtonSavior.BUTTON_TTS_ENGINE, content=self.ttsEngine)); 144 domOneButtonProgramRoot.append(ButtonSavior.createXMLElement(ButtonSavior.BUTTON_PLAY_ONCE, content=str(self.playOnce))); 145 return domOneButtonProgramRoot
146
147 148 # ----------------------------------------------- Class SpeakEasyController ------------------------------------ 149 150 -class SpeakEasyController(object):
151 ''' 152 Control logic behind the speakeasy GUI. 153 154 Available voices: 155 1. Festival: Usually voice_kal_diphone (male) on Ubuntu installations 156 2. Cepstral: Depends on your installation. Voices are individually licensed. 157 ''' 158 159 VERSION = '1.1'; 160 PID_PUBLICATION_FILE = "/tmp/speakeasyPID"; 161 162 # Mapping from sound button names ('SOUND_1', 'SOUND_2', etc) to sound filename (just basename): 163 soundPaths = {} 164 # Mapping sound file basenames to their full file names: 165 soundPathsFull = {}; 166 167 # Constant for repeating play of sound/music/voice forever: 168 FOREVER = -1; 169 170 # Unix signals for use with clearing text remotely, and with 171 # pasting and speech-triggering from remote: 172 REMOTE_CLEAR_TEXT_SIG = signal.SIGUSR1; 173 REMOTE_PASTE_AND_SPEAK_SIG = signal.SIGUSR2; 174
175 - def __init__(self, dirpath, unix_sig_notify_read_socket=None, stand_alone=None):
176 177 if stand_alone is None: 178 stand_alone = (DEFAULT_PLAY_LOCATION == PlayLocation.LOCALLY); 179 originalStandAlone = stand_alone 180 181 self.unix_sig_notify_read_socket = unix_sig_notify_read_socket; 182 self.stand_alone = stand_alone; 183 self.gui = None; 184 self.soundPlayer = None; 185 self.textToSpeechPlayer = None; 186 self.rosInitialized = False; 187 self.speechReplayDemons = []; 188 self.soundReplayDemons = []; 189 self.sound_file_names = []; 190 self.roboComm = None; 191 192 localInit = False; 193 robotInit = False; 194 195 # Remember original default play location, so that 196 # we can warn user of the import error condition further 197 # down, when the GUI is up: 198 DEFAULT_PLAY_LOCATION_ORIG = DEFAULT_PLAY_LOCATION; 199 200 if self.stand_alone: 201 localInit = self.initLocalOperation(); 202 else: # Robot operation 203 robotInit = self.initROSOperation(); 204 205 self.gui = SpeakEasyGUI(stand_alone=self.stand_alone, sound_effect_labels=self.sound_file_names); 206 self.gui.setWindowTitle("SpeakEasy (V" + SpeakEasyController.VERSION + ")"); 207 208 self.dialogService = DialogService(self.gui); 209 # Handler that makes program button temporarily 210 # look different to indicate entry into program mode: 211 self.gui.hideButtonSignal.connect(alternateLookHandler); 212 # Handler that makes program button look normal: 213 self.gui.showButtonSignal.connect(standardLookHandler); 214 215 # Now that we have the GUI up, we can warn user 216 # if ROS couldn't be imported, the ROS node wasn't running, 217 # or no SpeakEasy node was available, yet this app was 218 # set to control a Ros node (rather than running locally): 219 220 if originalStandAlone == PlayLocation.ROBOT and not robotInit: 221 self.dialogService.showErrorMsg("Application was set to control sound on robot, but: %s. Switching to local operation." % 222 str(self.rosInitException)); 223 224 if self.stand_alone: 225 self.gui.setWhereToPlay(PlayLocation.LOCALLY); 226 else: 227 self.gui.setWhereToPlay(PlayLocation.ROBOT); 228 229 self.dirpath = dirpath; 230 231 # No speech buttons programmed yet: 232 self.programs = {}; 233 234 self.currentButtonSetFile = os.path.join(speakeasy_persistence.ButtonSavior.SPEECH_SET_DIR,'default.xml'); 235 236 # Accept SIGUSR1 and SIGUSR2 from other processes 237 # to initiate PASTE and CLEAR operations, of the 238 # text area, respectively. Done via a socket from 239 # outside the application, which we connect to a QSocketNofifier 240 # (see __main__ below): 241 self.unixSigNotifier = QSocketNotifier(self.unix_sig_notify_read_socket.fileno(), QSocketNotifier.Read) 242 243 self.connectWidgetsToActions() 244 self.installDefaultSpeechSet(); 245 246 # Let other processes know out pid: 247 self.publishPID();
248 249 #---------------------------------- 250 # shutdown 251 #-------------- 252
253 - def shutdown(self):
254 ''' 255 Delete the PID pub file. Not crucial, but nice to let other 256 processes know that SpeakEasy is no longer running. 257 ''' 258 try: 259 os.remove(SpeakEasyController.PID_PUBLICATION_FILE); 260 except: 261 pass
262 263 #---------------------------------- 264 # initLocalOperation 265 #-------------- 266
267 - def initLocalOperation(self):
268 ''' 269 Initialize for playing sound and text-to-speech locally. 270 OK to call multiple times. Initializes 271 self.sound_file_names to a list of sound file names 272 for use with SoundPlayer instance. 273 @return: True if initialization succeeded, else False. 274 @rtype: boolean 275 ''' 276 if self.soundPlayer is None: 277 self.soundPlayer = SoundPlayer(); 278 if self.textToSpeechPlayer is None: 279 self.textToSpeechPlayer = TextToSpeechProvider(); 280 self.sound_file_names = self.getAvailableSoundEffectFileNames(stand_alone=True); 281 self.stand_alone = True; 282 return True;
283 284 #---------------------------------- 285 # initROSOperation 286 #-------------- 287
288 - def initROSOperation(self):
289 ''' 290 Try to initialize operation through ROS messages to 291 a SpeakEasy ROS node. If that init fails, revert to local 292 operation. 293 @return: True if ROS operation init succeeded. Else, if local ops was initiated instead, 294 return False. 295 @rtype: bool 296 ''' 297 # Try to initialize ROS. If that does not work, instantiation 298 # raises NotImplementedError, or IOError: 299 try: 300 self.roboComm = RoboComm(); 301 self.sound_file_names = self.roboComm.getSoundEffectNames(); 302 self.stand_alone = False; 303 return True 304 except Exception as rosInitFailure: 305 self.rosInitException = rosInitFailure; 306 # Robot init didn't work, fall back to local op: 307 self.initLocalOperation(); 308 return False
309 310 #---------------------------------- 311 # connectWidgetsToActions 312 #-------------- 313
314 - def connectWidgetsToActions(self):
315 self.gui.speechInputFld 316 for recorderButton in self.gui.recorderButtonDict.values(): 317 recorderButton.clicked.connect(partial(self.actionRecorderButtons, recorderButton)); 318 self.connectProgramButtonsToActions(); 319 for soundButton in self.gui.soundButtonDict.values(): 320 soundButton.clicked.connect(partial(self.actionSoundButtons, soundButton)); 321 newSpeechSetButton = self.gui.speechSetButtonDict[SpeakEasyGUI.interactionWidgets['NEW_SPEECH_SET']]; 322 newSpeechSetButton.clicked.connect(self.actionNewSpeechSet); 323 pickSpeechSetButton = self.gui.speechSetButtonDict[SpeakEasyGUI.interactionWidgets['PICK_SPEECH_SET']]; 324 pickSpeechSetButton.clicked.connect(self.actionPickSpeechSet); 325 326 # Location where to play: Locally, or at the Robot: 327 for radioButton in self.gui.playLocalityRadioButtonsDict.values(): 328 if radioButton.text() == "Play at robot": 329 radioButton.clicked.connect(partial(self.actionWhereToPlayRadioButton, PlayLocation.ROBOT)); 330 else: 331 radioButton.clicked.connect(partial(self.actionWhereToPlayRadioButton, PlayLocation.LOCALLY)); 332 333 self.gui.replayPeriodSpinBox.valueChanged.connect(self.actionRepeatPeriodChanged); 334 335 pasteButton = self.gui.convenienceButtonDict[SpeakEasyGUI.interactionWidgets['PASTE']]; 336 pasteButton.clicked.connect(self.actionPaste); 337 clearButton = self.gui.convenienceButtonDict[SpeakEasyGUI.interactionWidgets['CLEAR']]; 338 clearButton.clicked.connect(self.actionClear); 339 340 # Remote control of clearing text field, and speaking what's in the 341 # text field from other applications. Handled via Unix signals SIGUSR1 342 # and SIGUSR2. These are caught in handleOS_SIGUSR1_2() in __main__. The 343 # handler writes the respective signal number to the socket, which triggerse 344 # a socket notifier. We connect that notifier to a handler: 345 self.unixSigNotifier.activated.connect(self.actionUnixSigReceived);
346 347 @Slot(int)
348 - def actionUnixSigReceived(self, socket):
349 # Read the signal number (32 is the buff size): 350 (sigNumStr, socketAddr) = self.unix_sig_notify_read_socket.recvfrom(32); 351 sigNumStr = sigNumStr.strip(); 352 if sigNumStr == str(SpeakEasyController.REMOTE_CLEAR_TEXT_SIG): 353 self.actionClear(); 354 elif sigNumStr == str(SpeakEasyController.REMOTE_PASTE_AND_SPEAK_SIG): 355 self.actionPaste() 356 playButton = self.gui.recorderButtonDict[self.gui.interactionWidgets['PLAY_TEXT']]; 357 self.actionRecorderButtons(playButton);
358
360 for programButton in self.gui.programButtonDict.values(): 361 programButton.pressed.connect(partial(self.actionProgramButtons, programButton)); 362 programButton.released.connect(partial(self.actionProgramButtonRelease, programButton));
363 364 365 #---------------------------------- 366 # getAvailableSoundEffectFileNames 367 #-------------- 368
369 - def getAvailableSoundEffectFileNames(self, stand_alone=None):
370 ''' 371 Determine all the available sound effect files. If this process 372 operates stand-alone, the local '../../sounds' subdirectory is searched. 373 Else, in a ROS environment, the available sound effect file names 374 are obtained from the 'speech_capabilities_inquiry' service call. 375 @param stand_alone: False if referenced sounds are to be from the ROS environment. 376 @type stand_alone: boolean 377 @return: array of sound file basenames without extensions. E.g.: [rooster, birds, elephant] 378 @rtype: [string] 379 ''' 380 381 if stand_alone is None: 382 stand_alone = self.stand_alone; 383 384 # Standalone files are local to this process: 385 if stand_alone: 386 return self.getAvailableLocalSoundEffectFileNames(); 387 388 # Get sound effect names from SpeakEasy ROS node: 389 return self.roboComm.getSoundEffectNames();
390 391 #---------------------------------- 392 # getAvailableLocalSoundEffectFileNames 393 #-------------- 394
396 397 scriptDir = os.path.dirname(os.path.realpath(__file__)); 398 soundDir = os.path.join(scriptDir, "../../sounds"); 399 if not os.path.exists(soundDir): 400 raise ValueError("No sound files found.") 401 402 fileAndDirsList = os.listdir(soundDir); 403 fileList = []; 404 # Grab all usable sound file names: 405 for fileName in fileAndDirsList: 406 fileExtension = SpeakeasyUtils.fileExtension(fileName); 407 if (fileExtension == "wav") or (fileExtension == "ogg"): 408 fileList.append(fileName); 409 410 sound_file_basenames = []; 411 for (i, full_file_name) in enumerate(fileList): 412 baseName = os.path.basename(full_file_name); 413 SpeakEasyController.soundPaths['SOUND_' + str(i)] = full_file_name; 414 # Chop extension off the basename (e.g. rooster.wav --> rooster): 415 sound_file_basenames.append(os.path.splitext(os.path.basename(full_file_name))[0]); 416 # Map basename (e.g. 'rooster.wav') to its full file name. 417 self.soundPathsFull[baseName] = os.path.join(soundDir,full_file_name); 418 return sound_file_basenames;
419 420 421 #---------------------------------- 422 # sayText 423 #-------------- 424
425 - def sayText(self, text, voice, ttsEngine="festival", sayOnce=True, stand_alone=None):
426 ''' 427 Send message to SpeakEasy service to say text, with the 428 given voice, using the given text-to-speech engine. 429 </p> 430 If the voice parameter is the Festival voice 'Male' it is a special case in 431 that it refers to the Festival engine's "voice_kal_diphone". We convert this. 432 433 @param text: Text to be uttered by the tts engine 434 @type string 435 @param voice: Name of speaking voice to be used. 436 @type string 437 @param ttsEngine: Name of tts engine to use (e.g. "festival" (the default), "cepstral" 438 @type string 439 @param sayOnce: Whether repeat the utterance over and over, or just say it once. 440 @type bool 441 ''' 442 443 if stand_alone is None: 444 stand_alone = self.stand_alone; 445 446 if ttsEngine == "festival" and voice == "Male": 447 voice = "voice_kal_diphone"; 448 # Repeat over and over? Or say once? 449 if stand_alone: 450 if sayOnce: 451 try: 452 self.textToSpeechPlayer.say(text, voice, ttsEngine); 453 except ValueError: 454 self.dialogService.showErrorMsg("Voice '" + str(voice) + 455 "' is not supported by the text-to-speech engine '" + 456 str(ttsEngine) + "'."); 457 else: 458 self.speechReplayDemons.append(SpeakEasyController.SpeechReplayDemon(text, 459 voice, 460 ttsEngine, 461 self.gui.getPlayRepeatPeriod(), 462 self.textToSpeechPlayer)); 463 self.speechReplayDemons[-1].start(); 464 else: 465 if sayOnce: 466 self.roboComm.say(text, voice=voice, ttsEngine=ttsEngine); 467 else: 468 self.roboComm.say(text, voice=voice, ttsEngine=ttsEngine, numRepeats=SpeakEasyController.FOREVER, repeatPeriod=self.gui.getPlayRepeatPeriod()); 469 return;
470 471 #---------------------------------- 472 # 473 #-------------- 474 475 476 #---------------------------------- 477 # actionRecorderButtons 478 #-------------- 479
480 - def actionRecorderButtons(self, buttonObj):
481 ''' 482 Handler for one of the recorder buttons pushed: 483 Play Text, or Stop. 484 @param buttonObj: The button object that was pushed. 485 @type buttonObj: QPushButton 486 ''' 487 488 # Play button pushed? 489 buttonKey = self.gui.interactionWidgets['PLAY_TEXT']; 490 if buttonObj == self.gui.recorderButtonDict[buttonKey]: 491 # If nothing in text input field, error msg, and done: 492 if self.gui.speechInputFld.isEmpty(): 493 self.dialogService.showErrorMsg("Nothing to play; enter text in the text field."); 494 return; 495 496 # Got text in input fld. Which of the voices is checked? 497 voice = self.gui.activeVoice(); 498 if (voice == "voice_kal_diphone"): 499 ttsEngine = "festival" 500 else: 501 ttsEngine = "cepstral" 502 self.sayText(self.gui.speechInputFld.getText(), voice, ttsEngine, self.gui.playOnceChecked()); 503 return; 504 505 # Stop button pushed? 506 buttonKey = self.gui.interactionWidgets['STOP']; 507 if buttonObj == self.gui.recorderButtonDict[buttonKey]: 508 self.stopAll();
509 510 #---------------------------------- 511 # stopAll 512 #-------------- 513
514 - def stopAll(self):
515 if self.stand_alone: 516 if len(self.speechReplayDemons) > 0: 517 for speechDemon in self.speechReplayDemons: 518 speechDemon.stop(); 519 self.speechReplayDemons = []; 520 self.textToSpeechPlayer.stop(); 521 if len(self.soundReplayDemons) > 0: 522 for soundDemon in self.soundReplayDemons: 523 soundDemon.stop(); 524 self.soundReplayDemons = []; 525 526 self.soundPlayer.stop(); 527 self.textToSpeechPlayer.stop(); 528 else: # Robot op 529 self.roboComm.stopSaying(); 530 self.roboComm.stopSound(); 531 return;
532 533 #---------------------------------- 534 # actionWhereToPlayRadioButton 535 #-------------- 536
537 - def actionWhereToPlayRadioButton(self, playLocation):
538 if playLocation == PlayLocation.LOCALLY: 539 self.stopAll(); 540 self.initLocalOperation(); 541 elif playLocation == PlayLocation.ROBOT: 542 self.stopAll(); 543 self.initROSOperation();
544 545 #---------------------------------- 546 # actionProgramButtons 547 #-------------- 548
549 - def actionProgramButtons(self, buttonObj):
550 # Record press-down time: 551 self.programButtonPushedTime = time.time(); # fractional seconds till beginning of epoch 552 # Schedule the button to blink when the programming mode hold delay is over: 553 self.buttonBlinkTimer = Timer(SpeakEasyGUI.PROGRAM_BUTTON_HOLD_TIME, partial(self.gui.blinkButton, buttonObj, False)); 554 self.buttonBlinkTimer.start();
555 556 #---------------------------------- 557 # actionProgramButtonRelease 558 #-------------- 559
560 - def actionProgramButtonRelease(self, buttonObj):
561 timeNow = time.time(); # fractional seconds till beginning of epoch 562 self.buttonBlinkTimer.cancel(); 563 # Sometimes the down press seems to get missed, and then 564 # self.programButtonPushedTime is None. Likely that happens 565 # when buttons are clicked quickly: 566 if self.programButtonPushedTime is None: 567 self.programButtonPushedTime = timeNow; 568 holdTime = timeNow - self.programButtonPushedTime; 569 # Button no longer held down: 570 self.programButtonPushedTime = None; 571 572 # Held long enough to indicate intention to program?: 573 if holdTime >= SpeakEasyGUI.PROGRAM_BUTTON_HOLD_TIME: 574 self.programOneButton(buttonObj); 575 else: 576 self.playProgram(buttonObj);
577 578 #---------------------------------- 579 # programOneButton 580 #-------------- 581
582 - def programOneButton(self, buttonObj):
583 584 if self.gui.speechInputFld.isEmpty(): 585 self.dialogService.showErrorMsg("You need to enter text in the input panel to program a button."); 586 return; 587 588 newButtonLabel = self.gui.getNewButtonLabel(); 589 if newButtonLabel is not None: 590 self.gui.setButtonLabel(buttonObj,newButtonLabel); 591 592 textToSave = self.gui.speechInputFld.getText(); 593 if self.gui.activeVoice() == "voice_kal_diphone": 594 ttsEngine = "festival" 595 else: 596 ttsEngine = "cepstral" 597 programObj = ButtonProgram(buttonObj.text(), textToSave, self.gui.activeVoice(), ttsEngine, self.gui.playOnceChecked()); 598 599 self.programs[buttonObj] = programObj;
600 601 #---------------------------------- 602 # playProgram 603 #-------------- 604
605 - def playProgram(self, buttonObj):
606 607 program = None; 608 try: 609 program = self.programs[buttonObj]; 610 except KeyError: 611 self.dialogService.showErrorMsg("This button does not contain a program. Press-and-hold for " +\ 612 str(int(SpeakEasyGUI.PROGRAM_BUTTON_HOLD_TIME)) +\ 613 " seconds to program."); 614 return; 615 616 617 onlyPlayOnce = program.playOnce; 618 voice = program.activeVoice; 619 ttsEngine = program.ttsEngine; 620 self.sayText(program.getText(), voice, ttsEngine, onlyPlayOnce);
621 622 #---------------------------------- 623 # actionSoundButtons 624 #-------------- 625
626 - def actionSoundButtons(self, buttonObj):
627 628 soundIndx = 0; 629 while True: 630 soundKey = "SOUND_" + str(soundIndx); 631 try: 632 soundLabel = self.gui.interactionWidgets[soundKey]; 633 oneButtonObj = self.gui.soundButtonDict[soundLabel]; 634 except KeyError: 635 raise ValueError("Unknown widget passed to actionSoundButton() method: " + str(buttonObj)); 636 if buttonObj == oneButtonObj: 637 # For local operation, sound effect button labels are keys 638 # to a dict that maps to the local file names: 639 if self.stand_alone: 640 soundFile = SpeakEasyController.soundPaths[soundKey]; 641 else: 642 soundFile = buttonObj.text(); 643 break; 644 else: 645 soundIndx += 1; 646 647 if self.stand_alone: 648 originalSoundFile = soundFile; 649 try: 650 if not os.path.exists(soundFile): 651 try: 652 soundFile = self.soundPathsFull[soundFile] 653 except KeyError: 654 self.dialogService.showErrorMsg("Sound file %s not found. Searched %s/../../sounds." % (originalSoundFile, __file__)) 655 return; 656 soundInstance = self.soundPlayer.play(soundFile); 657 if self.gui.playRepeatedlyChecked(): 658 self.soundReplayDemons.append(SpeakEasyController.SoundReplayDemon(soundInstance,self.gui.getPlayRepeatPeriod(), self.soundPlayer)); 659 self.soundReplayDemons[-1].start(); 660 661 except IOError as e: 662 self.dialogService.showErrorMsg(str(e)); 663 return 664 else: # Robot operation 665 if self.gui.playRepeatedlyChecked(): 666 self.roboComm.playSound(soundFile, numRepeats=SpeakEasyController.FOREVER, repeatPeriod=self.gui.getPlayRepeatPeriod()); 667 else: 668 self.roboComm.playSound(soundFile);
669 670 # ------------------------- Changing and Adding Button Programs -------------- 671 672 #---------------------------------- 673 # actionNewSpeechSet 674 #-------------- 675 676
677 - def actionNewSpeechSet(self):
678 679 # Get an iterator over all the current program# button UI widgets: 680 programButtonIt = self.gui.programButtonIterator(); 681 682 # Get an array of ButtonProgram objects that are associated 683 # with those buttons: 684 buttonProgramArray = []; 685 while True: 686 try: 687 buttonObj = programButtonIt.next(); 688 buttonLabel = buttonObj.text(); 689 try: 690 buttonProgramArray.append(self.programs[buttonObj]); 691 except KeyError: 692 # Button was not programmed. Create an empty ButtonProgram: 693 buttonProgramArray.append(ButtonProgram(buttonLabel, "", "Male", "festival")); 694 except StopIteration: 695 break; 696 697 # Save this array of programs as XML: 698 makeNewFile = self.dialogService.newButtonSetOrUpdateCurrent(); 699 if makeNewFile == DialogService.ButtonSaveResult.CANCEL: 700 return; 701 if makeNewFile == DialogService.ButtonSaveResult.NEW_SET: 702 fileName = self.getNewSpeechSetName(); 703 ButtonSavior.saveToFile(buttonProgramArray, fileName, title=os.path.basename(fileName)); 704 self.currentButtonSetFile = fileName; 705 buttonSetNum = self.getSpeechSetFromSpeechFileName(fileName); 706 self.dialogService.showInfoMessage("New speech set %d created." % buttonSetNum); 707 elif makeNewFile == DialogService.ButtonSaveResult.UPDATE_CURRENT: 708 fileName = self.currentButtonSetFile; 709 ButtonSavior.saveToFile(buttonProgramArray, fileName, title=os.path.basename(fileName)); 710 if os.path.basename(fileName) != 'default.xml': 711 buttonSetNum = self.getSpeechSetFromSpeechFileName(self.currentButtonSetFile); 712 self.dialogService.showInfoMessage("Saved to speech button set %d." % buttonSetNum); 713 else: 714 self.dialogService.showInfoMessage("Saved to speech button set 'default.xml'"); 715 716 if os.path.basename(fileName) != 'default.xml': 717 try: 718 shutil.copy(fileName, os.path.join(ButtonSavior.SPEECH_SET_DIR, "default.xml")); 719 except: 720 rospy.logerr("Could not copy new program XML file to default.xml.");
721 722 #---------------------------------- 723 # actionPickSpeechSet 724 #-------------- 725
726 - def actionPickSpeechSet(self):
727 728 # Build an array of ButtonProgram instances for each 729 # of the XML files in the button set directory. Collect 730 # these arrays in buttonProgramArray: 731 732 xmlFileNames = self.getAllSpeechSetXMLFileNames(); 733 if xmlFileNames is None: 734 self.dialogService.showErrorMsg("No additional button sets are stored on your disk."); 735 return None; 736 737 # Fill the following array with arrays of ButtonProgram: 738 buttonProgramArrays = []; 739 for xmlFileName in xmlFileNames: 740 if xmlFileName == 'default.xml': 741 continue; 742 try: 743 (buttonSettingTitle, buttonProgram) = ButtonSavior.retrieveFromFile(xmlFileName, ButtonProgram); 744 buttonProgramArrays.append(buttonProgram); 745 except ValueError as e: 746 # Bad XML: 747 rospy.logerr(e.toStr()); 748 return; 749 750 buttonSetSelector = ButtonSetPopupSelector(iter(buttonProgramArrays)); 751 buttonSetSelected = buttonSetSelector.exec_(); 752 if buttonSetSelected == -1: 753 self.dialogService.showErrorMsg('No button sets have been defined yet.') 754 return; 755 756 # Get the selected ButtonProgram array: 757 buttonPrograms = buttonSetSelector.getCurrentlyShowingSet(); 758 self.replaceProgramButtons(buttonPrograms); 759 760 # Copy this new XML file into default.xml, so that it will be 761 # loaded next time the application starts: 762 763 ButtonSavior.saveToFile(buttonPrograms, os.path.join(ButtonSavior.SPEECH_SET_DIR, "default.xml"), title="default.xml");
764 765 766 #---------------------------------- 767 # actionRepeatPeriodChanged 768 #-------------- 769
770 - def actionRepeatPeriodChanged(self):
771 # If the repeat period is changed on its spinbox, 772 # automatically select 'Play repeatedly': 773 self.gui.setPlayRepeatedlyChecked();
774 775 #---------------------------------- 776 # actionClear 777 #-------------- 778
779 - def actionClear(self):
780 self.gui.speechInputFld.clear();
781 782 #---------------------------------- 783 # actionPaste 784 #-------------- 785
786 - def actionPaste(self):
787 # Also called by handleOS_SIGUSR1_2 788 textArea = self.gui.speechInputFld; 789 currCursor = textArea.textCursor(); 790 currCursor.insertText(QApplication.clipboard().text());
791 792 #---------------------------------- 793 # installDefaultSpeechSet 794 #-------------- 795
796 - def installDefaultSpeechSet(self):
797 defaultPath = os.path.join(ButtonSavior.SPEECH_SET_DIR, "default.xml"); 798 if not os.path.exists(defaultPath): 799 return; 800 (buttonSetTitle, buttonPrograms) = ButtonSavior.retrieveFromFile(defaultPath, ButtonProgram); 801 self.replaceProgramButtons(buttonPrograms);
802 803 #---------------------------------- 804 # replaceProgramButtons 805 #-------------- 806
807 - def replaceProgramButtons(self, buttonProgramArray):
808 self.gui.replaceProgramButtons(buttonProgramArray); 809 self.connectProgramButtonsToActions(); 810 # Update the button object --> ButtonProgram instance mapping: 811 self.programs = {}; 812 buttonObjIt = self.gui.programButtonIterator(); 813 for buttonProgram in buttonProgramArray: 814 try: 815 self.programs[buttonObjIt.next()] = buttonProgram; 816 except StopIteration: 817 # Should not happen: 818 raise ValueError("Fewer buttons than ButtonProgram instances.");
819 820 #---------------------------------- 821 # getAllSpeechSetXMLFileNames 822 #-------------- 823
824 - def getAllSpeechSetXMLFileNames(self):
825 826 if not os.path.exists(ButtonSavior.SPEECH_SET_DIR): 827 return None; 828 829 xmlFileNames = [] 830 for fileName in os.listdir(ButtonSavior.SPEECH_SET_DIR): 831 if fileName.endswith(".xml") or fileName.endswith(".XML"): 832 xmlFileNames.append(fileName); 833 if len(xmlFileNames) == 0: 834 return None; 835 836 return xmlFileNames;
837 838 #---------------------------------- 839 # getNewSpeechSetName 840 #-------------- 841
842 - def getNewSpeechSetName(self):
843 844 if not os.path.exists(ButtonSavior.SPEECH_SET_DIR): 845 os.makedirs(ButtonSavior.SPEECH_SET_DIR); 846 suffix = 1; 847 newFileName = "buttonProgram1.xml"; 848 for filename in os.listdir(ButtonSavior.SPEECH_SET_DIR): 849 if filename == 'default.xml': 850 continue; 851 if filename == newFileName: 852 suffix += 1; 853 newFileName = "buttonProgram" + str(suffix) + ".xml"; 854 continue; 855 break; 856 return os.path.join(ButtonSavior.SPEECH_SET_DIR, newFileName);
857 858 #---------------------------------- 859 # getSpeechSetFromSpeechFileName 860 #-------------- 861
862 - def getSpeechSetFromSpeechFileName(self, filePath):
863 ''' 864 Given a file path to a button set, return the button set's number. 865 We assume that the path is well formed, and all button sets are named 866 buttonProgramnnn.xml. If malformed path, shows error msg on screen, and 867 returns None. 868 @param filePath: Path to xml file. 869 @type filePath: string 870 @return: Number encoded in file name (i.e. nnn) 871 @rtype: int 872 ''' 873 874 # Get something like: buttonSet2.xml: 875 fileName = os.path.basename(filePath).split('.')[0]; 876 buttonSetNum = fileName[len('buttonProgram'):]; 877 try: 878 return int(buttonSetNum) 879 except ValueError: 880 self.dialogService.showErrorMsg('Bad file path to button sets: %s' % filePath); 881 return None;
882 883 884 #---------------------------------- 885 # publishPID 886 #-------------- 887
888 - def publishPID(self):
889 with open(SpeakEasyController.PID_PUBLICATION_FILE,'w') as fd: 890 fd.write(str(os.getpid()));
891 892 #---------------------------------- 893 # handleOS_SIGUSR1_2 894 #-------------- 895
896 - def handleOS_SIGUSR1_2(self, signum, stack):
897 if signum == signal.SIGUSR1: 898 self.actionPaste; 899 elif signum == signal.SIGUSR2: 900 self.actionClear();
901 902 # -------------------------------------------- Replay Demon ------------------------------- 903 904 # Only used for local operation:
905 - class ReplayDemon(threading.Thread):
906
907 - def __init__(self, repeatPeriod):
908 super(SpeakEasyController.ReplayDemon, self).__init__(); 909 self.repeatPeriod = repeatPeriod; 910 self.stopped = True;
911
912 - class SoundReplayDemon(ReplayDemon):
913
914 - def __init__(self, soundInstance, repeatPeriod, soundPlayer):
915 super(SpeakEasyController.SoundReplayDemon, self).__init__(repeatPeriod); 916 self.soundInstance = soundInstance; 917 self.soundPlayer = soundPlayer;
918
919 - def run(self):
920 self.stopped = False; 921 self.soundPlayer.waitForSoundDone(self.soundInstance); 922 while not self.stopped: 923 time.sleep(self.repeatPeriod); 924 self.soundPlayer.play(self.soundInstance, blockTillDone=True);
925
926 - def stop(self):
927 self.stopped = True; 928 self.soundPlayer.stop(self.soundInstance);
929
930 - class SpeechReplayDemon(ReplayDemon):
931
932 - def __init__(self, text, voiceName, ttsEngine, repeatPeriod, textToSpeechPlayer):
933 super(SpeakEasyController.SpeechReplayDemon, self).__init__(repeatPeriod); 934 self.text = text; 935 self.ttsEngine = ttsEngine; 936 self.voiceName = voiceName; 937 self.textToSpeechPlayer = textToSpeechPlayer;
938 939
940 - def run(self):
941 self.stopped = False; 942 self.textToSpeechPlayer.waitForSoundDone(); 943 while not self.stopped: 944 time.sleep(self.repeatPeriod); 945 try: 946 self.textToSpeechPlayer.say(self.text, self.voiceName, self.ttsEngine, blockTillDone=True); 947 except: 948 # If any problem, stop this thread so that we don't keep 949 # generating that same error: 950 self.stop();
951
952 - def stop(self):
953 self.stopped = True; 954 self.textToSpeechPlayer.stop();
955 956 if __name__ == "__main__": 957 958 # Create socket pair to communicate between the 959 # Unix signal handler and the Qt event loop: 960 rsock, wsock = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
961 962 # Handler for SIGUSR1 and SIGUSR2 963 - def sigusr1_2_handler(signum, stack):
964 print 'Received Signal:', signum 965 wsock.send(str(signum) + "\n");
966 967 app = QApplication(sys.argv); 968 969 # To find the sounds, we need the absolute directory 970 # path to this script: 971 scriptDir = os.path.dirname(os.path.abspath(sys.argv[0])); 972 #speakeasyController = SpeakEasyController(scriptDir, stand_alone=False); 973 #speakeasyController = SpeakEasyController(scriptDir, stand_alone=True); 974 975 if len(sys.argv) > 1: 976 if sys.argv[1] == 'local': 977 print "Starting SpeakEasy in local (i.e. non-ROS) mode." 978 speakeasyController = SpeakEasyController(scriptDir, unix_sig_notify_read_socket=rsock, stand_alone=True); 979 else: 980 try: 981 rospy.loginfo("Will attempt to start SpeakEasy in ROS mode. If fail, switch to local mode. Possibly a few seconds delay..."); 982 except: 983 print("Will attempt to start SpeakEasy in ROS mode. If fail, switch to local mode. Possibly a few seconds delay..."); 984 speakeasyController = SpeakEasyController(scriptDir, unix_sig_notify_read_socket=rsock, stand_alone=None); 985 else: 986 try: 987 rospy.loginfo("Will attempt to start SpeakEasy in ROS mode. If fail, switch to local mode. Possibly a few seconds delay..."); 988 except: 989 print("Will attempt to start SpeakEasy in ROS mode. If fail, switch to local mode. Possibly a few seconds delay..."); 990 speakeasyController = SpeakEasyController(scriptDir, unix_sig_notify_read_socket=rsock, stand_alone=None); 991 992 # Attach Unix signals USR1/USR2 to the sigusr1_2_handler(). 993 # (These signals are separate from the Qt signals!): 994 signal.signal(signal.SIGUSR1, sigusr1_2_handler) 995 signal.signal(signal.SIGUSR2, sigusr1_2_handler) 996 # Unix signals are delivered to Qt only when Qt 997 # leaves its event loop. Force that to happen 998 # every half second: 999 timer = QTimer() 1000 timer.start(500) 1001 timer.timeout.connect(lambda: None) 1002 1003 # Enter Qt application main loop 1004 sys.exit(app.exec_()); 1005