| Home | Trees | Indices | Help |
|---|
|
|
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
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
98
99 #----------------------------------
100 # setText
101 #--------------
102
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
117
118 #----------------------------------
119 # getTtsEngine
120 #--------------
121
124
125 #----------------------------------
126 # getVoice
127 #--------------
128
130 return self.activeVoice;
131
132 #----------------------------------
133 # toXML
134 #--------------
135
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
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
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
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
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
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)
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
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
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
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
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
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
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
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
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
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
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
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
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
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
781
782 #----------------------------------
783 # actionPaste
784 #--------------
785
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
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
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
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
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
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
891
892 #----------------------------------
893 # handleOS_SIGUSR1_2
894 #--------------
895
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:
906
908 super(SpeakEasyController.ReplayDemon, self).__init__();
909 self.repeatPeriod = repeatPeriod;
910 self.stopped = True;
911
913
915 super(SpeakEasyController.SoundReplayDemon, self).__init__(repeatPeriod);
916 self.soundInstance = soundInstance;
917 self.soundPlayer = soundPlayer;
918
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
929
931
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
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
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)
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
| Home | Trees | Indices | Help |
|---|
| Generated by Epydoc 3.0.1 on Mon Nov 26 18:08:32 2012 | http://epydoc.sourceforge.net |