1 |
|
# Orca |
2 |
|
# |
3 |
|
# Copyright 2005-2007 Sun Microsystems Inc. |
4 |
|
# |
5 |
|
# This library is free software; you can redistribute it and/or |
6 |
|
# modify it under the terms of the GNU Library General Public |
7 |
|
# License as published by the Free Software Foundation; either |
8 |
|
# version 2 of the License, or (at your option) any later version. |
9 |
|
# |
10 |
|
# This library is distributed in the hope that it will be useful, |
11 |
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 |
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
13 |
|
# Library General Public License for more details. |
14 |
|
# |
15 |
|
# You should have received a copy of the GNU Library General Public |
16 |
|
# License along with this library; if not, write to the |
17 |
|
# Free Software Foundation, Inc., 59 Temple Place - Suite 330, |
18 |
|
# Boston, MA 02111-1307, USA. |
19 |
|
|
20 |
1 |
"""Provides a SpeechServer factory for gnome-speech drivers.""" |
21 |
|
|
22 |
1 |
__id__ = "$Id: gnomespeechfactory.py 2126 2007-03-06 21:35:17Z richb $" |
23 |
1 |
__version__ = "$Revision: 2126 $" |
24 |
1 |
__date__ = "$Date: 2007-03-06 13:35:17 -0800 (Tue, 06 Mar 2007) $" |
25 |
1 |
__copyright__ = "Copyright (c) 2005-2006 Sun Microsystems Inc." |
26 |
1 |
__license__ = "LGPL" |
27 |
|
|
28 |
1 |
import logging |
29 |
1 |
log = logging.getLogger("speech") |
30 |
|
|
31 |
1 |
import os |
32 |
|
|
33 |
1 |
import gobject |
34 |
1 |
import Queue |
35 |
1 |
import string |
36 |
1 |
import threading |
37 |
1 |
import time |
38 |
|
|
39 |
1 |
import bonobo |
40 |
|
|
41 |
1 |
import atspi |
42 |
1 |
import chnames |
43 |
1 |
import debug |
44 |
1 |
import orca_state |
45 |
1 |
import punctuation_settings |
46 |
1 |
import settings |
47 |
1 |
import speech |
48 |
1 |
import speechserver |
49 |
|
|
50 |
1 |
from acss import ACSS |
51 |
|
|
52 |
1 |
from orca_i18n import _ # for gettext support |
53 |
|
|
54 |
1 |
atspi.ORBit.load_typelib('GNOME_Speech') |
55 |
1 |
import GNOME.Speech, GNOME__POA.Speech |
56 |
|
|
57 |
2 |
class _SayAll: |
58 |
1 |
def __init__(self, iterator, context, id, progressCallback): |
59 |
0 |
self.utteranceIterator = iterator |
60 |
0 |
self.currentContext = context |
61 |
0 |
self.idForCurrentContext = id |
62 |
0 |
self.progressCallback = progressCallback |
63 |
|
|
64 |
2 |
class _Speaker(GNOME__POA.Speech.SpeechCallback): |
65 |
|
"""Implements gnome-speech's SpeechCallback class. The gnome-speech |
66 |
|
server only allows one speech callback to be registered with a speaker |
67 |
|
and there's no way to unregister it. So...we need to handle stuff |
68 |
|
like that on our own. This class handles this for us and also delegates |
69 |
|
all calls to the 'real' gnome speech speaker. |
70 |
|
""" |
71 |
|
|
72 |
1 |
def __init__(self, gnome_speaker): |
73 |
0 |
self.gnome_speaker = gnome_speaker |
74 |
0 |
if settings.enableSpeechCallbacks: |
75 |
0 |
gnome_speaker.registerSpeechCallback(self._this()) |
76 |
0 |
self.__callbacks = [] |
77 |
|
|
78 |
1 |
def registerCallback(self, callback): |
79 |
0 |
self.__callbacks.append(callback) |
80 |
|
|
81 |
1 |
def deregisterCallback(self, callback): |
82 |
0 |
self.__callbacks.remove(callback) |
83 |
|
|
84 |
1 |
def notify(self, type, id, offset): |
85 |
|
"""Called by GNOME Speech when the GNOME Speech driver generates |
86 |
|
a callback. |
87 |
|
|
88 |
|
Arguments: |
89 |
|
- type: one of GNOME.Speech.speech_callback_speech_started, |
90 |
|
GNOME.Speech.speech_callback_speech_progress, |
91 |
|
GNOME.Speech.speech_callback_speech_ended |
92 |
|
- id: the id of the utterance (returned by say) |
93 |
|
- offset: the character offset into the utterance (for progress) |
94 |
|
""" |
95 |
0 |
for callback in self.__callbacks: |
96 |
0 |
callback.notify(type, id, offset) |
97 |
|
|
98 |
1 |
def say(self, text): |
99 |
0 |
return self.gnome_speaker.say(text) |
100 |
|
|
101 |
1 |
def stop(self): |
102 |
0 |
self.gnome_speaker.stop() |
103 |
|
|
104 |
1 |
def getSupportedParameters(self): |
105 |
0 |
return self.gnome_speaker.getSupportedParameters() |
106 |
|
|
107 |
1 |
def getParameterValue(self, name): |
108 |
0 |
return self.gnome_speaker.getParameterValue(name) |
109 |
|
|
110 |
1 |
def setParameterValue(self, name, value): |
111 |
0 |
return self.gnome_speaker.setParameterValue(name, value) |
112 |
|
|
113 |
1 |
def unref(self): |
114 |
0 |
try: |
115 |
0 |
self.gnome_speaker.unref() |
116 |
0 |
except: |
117 |
0 |
pass |
118 |
|
|
119 |
2 |
class SpeechServer(speechserver.SpeechServer): |
120 |
|
"""Provides SpeechServer implementation for gnome-speech.""" |
121 |
|
|
122 |
|
# Dictionary of known running servers. The key is the iid and the |
123 |
|
# value is the SpeechServer instance. We do this to enforce a |
124 |
|
# singleton instance of any given server. |
125 |
|
# |
126 |
1 |
__activeServers = {} |
127 |
|
|
128 |
1 |
def __activateDriver(iid): |
129 |
2 |
driver = bonobo.activation.activate_from_id(iid, |
130 |
2 |
0, |
131 |
2 |
False) |
132 |
2 |
driver = driver._narrow(GNOME.Speech.SynthesisDriver) |
133 |
2 |
isInitialized = driver.isInitialized() |
134 |
2 |
if not isInitialized: |
135 |
2 |
isInitialized = driver.driverInit() |
136 |
2 |
if not isInitialized: |
137 |
1 |
try: |
138 |
1 |
driver.unref() |
139 |
0 |
except: |
140 |
0 |
pass |
141 |
1 |
driver = None |
142 |
2 |
return driver |
143 |
|
|
144 |
1 |
__activateDriver = staticmethod(__activateDriver) |
145 |
|
|
146 |
1 |
def __createServer(iid): |
147 |
2 |
server = None |
148 |
|
|
149 |
2 |
if SpeechServer.__activeServers.has_key(iid): |
150 |
0 |
server = SpeechServer.__activeServers[iid] |
151 |
|
else: |
152 |
2 |
driver = SpeechServer.__activateDriver(iid) |
153 |
2 |
if driver: |
154 |
1 |
server = SpeechServer(driver, iid) |
155 |
1 |
SpeechServer.__activeServers[iid] = server |
156 |
|
|
157 |
2 |
return server |
158 |
|
|
159 |
1 |
__createServer = staticmethod(__createServer) |
160 |
|
|
161 |
1 |
def getFactoryName(): |
162 |
|
"""Returns a localized name describing this factory.""" |
163 |
1 |
return _("GNOME Speech Services") |
164 |
|
|
165 |
1 |
getFactoryName = staticmethod(getFactoryName) |
166 |
|
|
167 |
1 |
def getSpeechServers(): |
168 |
|
"""Gets available speech servers as a list. The caller |
169 |
|
is responsible for calling the shutdown() method of each |
170 |
|
speech server returned. |
171 |
|
""" |
172 |
|
|
173 |
|
# Get a list of all the drivers on the system and find out how many |
174 |
|
# of them work. |
175 |
|
# |
176 |
1 |
knownServers = bonobo.activation.query( |
177 |
|
"repo_ids.has('IDL:GNOME/Speech/SynthesisDriver:0.3')") |
178 |
|
|
179 |
3 |
for server in knownServers: |
180 |
2 |
if not SpeechServer.__activeServers.has_key(server.iid): |
181 |
2 |
try: |
182 |
2 |
SpeechServer.__createServer(server.iid) |
183 |
0 |
except: |
184 |
0 |
debug.printException(debug.LEVEL_WARNING) |
185 |
|
|
186 |
1 |
return SpeechServer.__activeServers.values() |
187 |
|
|
188 |
1 |
getSpeechServers = staticmethod(getSpeechServers) |
189 |
|
|
190 |
1 |
def getSpeechServer(info=None): |
191 |
|
"""Gets a given SpeechServer based upon the info. |
192 |
|
See SpeechServer.getInfo() for more info. |
193 |
|
""" |
194 |
|
|
195 |
0 |
if info and SpeechServer.__activeServers.has_key(info[1]): |
196 |
0 |
return SpeechServer.__activeServers[info[1]] |
197 |
|
|
198 |
0 |
server = None |
199 |
|
|
200 |
0 |
gservers = bonobo.activation.query( |
201 |
|
"repo_ids.has('IDL:GNOME/Speech/SynthesisDriver:0.3')") |
202 |
|
|
203 |
0 |
if len(gservers) == 0: |
204 |
0 |
return None |
205 |
|
|
206 |
0 |
gserver = None |
207 |
|
|
208 |
|
# All this logic is to attempt to fall back to a working |
209 |
|
# driver if the desired one cannot be found or is not |
210 |
|
# not working. |
211 |
|
# |
212 |
0 |
if not info: |
213 |
0 |
gserver = gservers[0] |
214 |
|
else: |
215 |
0 |
for s in gservers: |
216 |
0 |
if s.iid == info[1]: |
217 |
0 |
gserver = s |
218 |
0 |
break |
219 |
|
|
220 |
0 |
if not gserver: |
221 |
0 |
return None |
222 |
|
|
223 |
0 |
server = None |
224 |
0 |
try: |
225 |
0 |
server = SpeechServer.__createServer(gserver.iid) |
226 |
0 |
except: |
227 |
0 |
debug.printException(debug.LEVEL_WARNING) |
228 |
|
|
229 |
0 |
if not server: |
230 |
0 |
for s in gservers: |
231 |
0 |
try: |
232 |
0 |
server = SpeechServer.__createServer(s.iid) |
233 |
0 |
if server: |
234 |
0 |
break |
235 |
0 |
except: |
236 |
0 |
debug.printException(debug.LEVEL_WARNING) |
237 |
|
|
238 |
0 |
return server |
239 |
|
|
240 |
1 |
getSpeechServer = staticmethod(getSpeechServer) |
241 |
|
|
242 |
1 |
def shutdownActiveServers(): |
243 |
|
"""Cleans up and shuts down this factory. |
244 |
|
""" |
245 |
0 |
for key in SpeechServer.__activeServers.keys(): |
246 |
0 |
server = SpeechServer.__activeServers[key] |
247 |
0 |
server.shutdown() |
248 |
|
|
249 |
1 |
shutdownActiveServers = staticmethod(shutdownActiveServers) |
250 |
|
|
251 |
1 |
def __init__(self, driver, iid): |
252 |
1 |
speechserver.SpeechServer.__init__(self) |
253 |
1 |
self.__speakers = {} |
254 |
1 |
self.__pitchInfo = {} |
255 |
1 |
self.__rateInfo = {} |
256 |
1 |
self.__volumeInfo = {} |
257 |
1 |
self.__driver = driver |
258 |
1 |
self.__driverName = driver.driverName |
259 |
1 |
self.__iid = iid |
260 |
1 |
self.__sayAll = None |
261 |
1 |
self.__isSpeaking = False |
262 |
1 |
self.__eventQueue = Queue.Queue(0) |
263 |
1 |
self.__gidleId = 0 |
264 |
1 |
self.__gidleLock = threading.Lock() |
265 |
1 |
self.__lastResetTime = 0 |
266 |
|
|
267 |
1 |
def __getRate(self, speaker): |
268 |
|
"""Gets the voice-independent ACSS rate value of a voice.""" |
269 |
|
|
270 |
0 |
if not self.__rateInfo.has_key(speaker): |
271 |
0 |
return 50.0 |
272 |
|
|
273 |
0 |
[minRate, averageRate, maxRate] = self.__rateInfo[speaker] |
274 |
0 |
rate = speaker.getParameterValue("rate") |
275 |
0 |
if rate < averageRate: |
276 |
0 |
return 50.0 * (rate - minRate) / (averageRate - minRate) |
277 |
0 |
elif rate > averageRate: |
278 |
0 |
return 50.0 \ |
279 |
|
+ (50.0 * (rate - averageRate) / (maxRate - averageRate)) |
280 |
|
else: |
281 |
0 |
return 50.0 |
282 |
|
|
283 |
1 |
def __setRate(self, speaker, acssRate): |
284 |
|
"""Determines the voice-specific rate setting for the |
285 |
|
voice-independent ACSS rate value. |
286 |
|
""" |
287 |
|
|
288 |
0 |
if not self.__rateInfo.has_key(speaker): |
289 |
0 |
return |
290 |
|
|
291 |
0 |
[minRate, averageRate, maxRate] = self.__rateInfo[speaker] |
292 |
0 |
if acssRate < 50.0: |
293 |
0 |
rate = minRate + acssRate * (averageRate - minRate) / 50.0 |
294 |
0 |
elif acssRate > 50.0: |
295 |
0 |
rate = averageRate \ |
296 |
|
+ (acssRate - 50.0) * (maxRate - averageRate) / 50.0 |
297 |
|
else: |
298 |
0 |
rate = averageRate |
299 |
|
|
300 |
0 |
speaker.setParameterValue("rate", rate) |
301 |
|
|
302 |
1 |
def __getPitch(self, speaker): |
303 |
|
"""Gets the voice-specific pitch setting for the |
304 |
|
voice-independent ACSS pitch value. |
305 |
|
|
306 |
|
Returns the voice-specific pitch setting. |
307 |
|
""" |
308 |
|
|
309 |
0 |
if not self.__pitchInfo.has_key(speaker): |
310 |
0 |
return 5.0 |
311 |
|
|
312 |
0 |
[minPitch, averagePitch, maxPitch] = self.__pitchInfo[speaker] |
313 |
0 |
pitch = speaker.getParameterValue("pitch") |
314 |
0 |
if pitch < averagePitch: |
315 |
0 |
return 5.0 * (pitch - minPitch) / (averagePitch - minPitch) |
316 |
0 |
elif pitch > averagePitch: |
317 |
0 |
return 5.0 \ |
318 |
|
+ (5.0 * (pitch - averagePitch) / (maxPitch - averagePitch)) |
319 |
|
else: |
320 |
0 |
return 5.0 |
321 |
|
|
322 |
1 |
def __setPitch(self, speaker, acssPitch): |
323 |
|
"""Determines the voice-specific pitch setting for the |
324 |
|
voice-independent ACSS pitch value. |
325 |
|
""" |
326 |
|
|
327 |
0 |
if not self.__pitchInfo.has_key(speaker): |
328 |
0 |
return |
329 |
|
|
330 |
0 |
[minPitch, averagePitch, maxPitch] = self.__pitchInfo[speaker] |
331 |
0 |
if acssPitch < 5.0: |
332 |
0 |
pitch = minPitch + acssPitch * (averagePitch - minPitch) / 5.0 |
333 |
0 |
elif acssPitch > 5.0: |
334 |
0 |
pitch = averagePitch \ |
335 |
|
+ (acssPitch - 5.0) * (maxPitch - averagePitch) / 5.0 |
336 |
|
else: |
337 |
0 |
pitch = averagePitch |
338 |
|
|
339 |
0 |
speaker.setParameterValue("pitch", pitch) |
340 |
|
|
341 |
1 |
def __setVolume(self, speaker, acssGain): |
342 |
|
"""Determines the voice-specific rate setting for the |
343 |
|
voice-independent ACSS rate value. |
344 |
|
""" |
345 |
|
|
346 |
0 |
if not self.__volumeInfo.has_key(speaker): |
347 |
0 |
return |
348 |
|
|
349 |
0 |
[minVolume, averageVolume, maxVolume] = self.__volumeInfo[speaker] |
350 |
0 |
volume = minVolume + acssGain * (maxVolume - minVolume) / 10.0 |
351 |
|
|
352 |
0 |
speaker.setParameterValue("volume", volume) |
353 |
|
|
354 |
1 |
def __getSpeaker(self, acss=None): |
355 |
|
|
356 |
0 |
voices = settings.voices |
357 |
0 |
defaultACSS = voices[settings.DEFAULT_VOICE] |
358 |
|
|
359 |
0 |
if not acss: |
360 |
0 |
acss = defaultACSS |
361 |
|
|
362 |
0 |
if self.__speakers.has_key(acss.name()): |
363 |
0 |
return self.__speakers[acss.name()] |
364 |
|
|
365 |
|
# Search for matching languages first, as that is the most |
366 |
|
# important thing. We also try to look for the desired language |
367 |
|
# first, but then fall back to the default language. |
368 |
|
# |
369 |
0 |
languages = [] |
370 |
0 |
try: |
371 |
0 |
if acss.has_key(ACSS.FAMILY): |
372 |
0 |
family = acss[ACSS.FAMILY] |
373 |
0 |
languages = [family[speechserver.VoiceFamily.LOCALE]] |
374 |
0 |
elif defaultACSS.has_key(ACSS.FAMILY): |
375 |
0 |
family = defaultACSS[ACSS.FAMILY] |
376 |
0 |
languages = [family[speechserver.VoiceFamily.LOCALE]] |
377 |
0 |
except: |
378 |
0 |
pass |
379 |
|
|
380 |
|
# [[[TODO: WDW - I'm using an array of languages here because |
381 |
|
# it will give us the opportunity to provide a match for |
382 |
|
# gnome-speech driver implementations that do not designate |
383 |
|
# languages using the combination of ISO 639-1 and 3166-1 |
384 |
|
# language_region strings (e.g., they use 'english' instead of |
385 |
|
# "en_US"). Thus, we could detect the default language is |
386 |
|
# "en_US" and just automatically append "english" and such to |
387 |
|
# the list to allow us to accomodate these kind of |
388 |
|
# variants.]]] |
389 |
|
# |
390 |
0 |
if len(languages) == 0: |
391 |
0 |
import locale |
392 |
0 |
language, encoding = locale.getdefaultlocale() |
393 |
0 |
languages = [language] |
394 |
|
|
395 |
0 |
voices = self.__driver.getAllVoices() |
396 |
0 |
foundVoices = [] |
397 |
0 |
for voice in voices: |
398 |
0 |
if voice.language in languages: |
399 |
0 |
foundVoices.append(voice) |
400 |
|
|
401 |
|
# If we didn't find any matches, well...punt. |
402 |
|
# |
403 |
0 |
if len(foundVoices): |
404 |
0 |
voices = foundVoices |
405 |
|
else: |
406 |
0 |
voices = self.__driver.getAllVoices() |
407 |
|
|
408 |
|
# Now search for a matching family name. |
409 |
|
# |
410 |
0 |
familyName = None |
411 |
0 |
if acss.has_key(ACSS.FAMILY): |
412 |
0 |
family = acss[ACSS.FAMILY] |
413 |
0 |
familyName = family[speechserver.VoiceFamily.NAME] |
414 |
0 |
elif defaultACSS.has_key(ACSS.FAMILY): |
415 |
0 |
family = defaultACSS[ACSS.FAMILY] |
416 |
0 |
familyName = family[speechserver.VoiceFamily.NAME] |
417 |
|
|
418 |
0 |
found = False |
419 |
0 |
for voice in voices: |
420 |
0 |
if (not familyName) or (voice.name == familyName): |
421 |
0 |
found = True |
422 |
0 |
break |
423 |
|
|
424 |
0 |
if not found: |
425 |
0 |
if len(voices) == 0: |
426 |
0 |
return None |
427 |
|
else: |
428 |
0 |
voice = voices[0] |
429 |
|
|
430 |
0 |
s = self.__driver.createSpeaker(voice) |
431 |
0 |
speaker = _Speaker(s._narrow(GNOME.Speech.Speaker)) |
432 |
|
|
433 |
|
# Turn off punctuation if the speaker allows us to do so. We |
434 |
|
# do this because we want to handle spoken punctuation on our |
435 |
|
# own. |
436 |
|
# |
437 |
0 |
try: |
438 |
0 |
speaker.setParameterValue("punctuation mode", 0.0) |
439 |
0 |
except: |
440 |
0 |
pass |
441 |
|
|
442 |
0 |
speaker.registerCallback(self) |
443 |
|
|
444 |
0 |
parameters = speaker.getSupportedParameters() |
445 |
0 |
for parameter in parameters: |
446 |
0 |
if parameter.name == "rate": |
447 |
0 |
self.__rateInfo[speaker] = \ |
448 |
|
[parameter.min, parameter.current, parameter.max] |
449 |
0 |
elif parameter.name == "pitch": |
450 |
0 |
self.__pitchInfo[speaker] = \ |
451 |
|
[parameter.min, parameter.current, parameter.max] |
452 |
0 |
elif parameter.name == "volume": |
453 |
0 |
self.__volumeInfo[speaker] = \ |
454 |
|
[parameter.min, parameter.current, parameter.max] |
455 |
|
|
456 |
0 |
if acss.has_key(ACSS.RATE): |
457 |
0 |
self.__setRate(speaker, acss[ACSS.RATE]) |
458 |
|
|
459 |
0 |
if acss.has_key(ACSS.AVERAGE_PITCH): |
460 |
0 |
self.__setPitch(speaker, acss[ACSS.AVERAGE_PITCH]) |
461 |
|
|
462 |
0 |
if acss.has_key(ACSS.GAIN): |
463 |
0 |
self.__setVolume(speaker, acss[ACSS.GAIN]) |
464 |
|
|
465 |
0 |
self.__speakers[acss.name()] = speaker |
466 |
|
|
467 |
0 |
return speaker |
468 |
|
|
469 |
1 |
def __idleHandler(self): |
470 |
|
"""Called by the gidle thread when there is something to do. |
471 |
|
The goal is to try to do all AT-SPI interactions on the gidle |
472 |
|
thread as a means to help prevent hangs.""" |
473 |
|
|
474 |
|
# Added in the notify method below. |
475 |
|
# |
476 |
|
# id = the id of the utterance we sent to gnome-speech |
477 |
|
# type = the type of progress we're getting |
478 |
|
# offset = character offset into the utterance |
479 |
|
# |
480 |
0 |
(id, type, offset) = self.__eventQueue.get() |
481 |
|
|
482 |
0 |
if self.__sayAll: |
483 |
0 |
if self.__sayAll.idForCurrentContext == id: |
484 |
0 |
context = self.__sayAll.currentContext |
485 |
0 |
if type == GNOME.Speech.speech_callback_speech_started: |
486 |
0 |
context.currentOffset = context.startOffset |
487 |
0 |
self.__sayAll.progressCallback( |
488 |
|
self.__sayAll.currentContext, |
489 |
0 |
speechserver.SayAllContext.PROGRESS) |
490 |
0 |
elif type == GNOME.Speech.speech_callback_speech_progress: |
491 |
0 |
context.currentOffset = context.startOffset + offset |
492 |
0 |
self.__sayAll.progressCallback( |
493 |
|
self.__sayAll.currentContext, |
494 |
0 |
speechserver.SayAllContext.PROGRESS) |
495 |
0 |
elif type == GNOME.Speech.speech_callback_speech_ended: |
496 |
0 |
try: |
497 |
0 |
[self.__sayAll.currentContext, acss] = \ |
498 |
|
self.__sayAll.utteranceIterator.next() |
499 |
0 |
debug.println(debug.LEVEL_INFO, |
500 |
0 |
"SPEECH OUTPUT: '" \ |
501 |
|
+ self.__sayAll.currentContext.utterance\ |
502 |
|
+ "'") |
503 |
0 |
log.info("'%s'" \ |
504 |
|
% self.__sayAll.currentContext.utterance) |
505 |
0 |
self.__sayAll.idForCurrentContext = self.__speak( |
506 |
|
self.__sayAll.currentContext.utterance, |
507 |
0 |
acss) |
508 |
0 |
except StopIteration: |
509 |
0 |
self.__isSpeaking = False |
510 |
0 |
context.currentOffset = context.endOffset |
511 |
0 |
self.__sayAll.progressCallback( |
512 |
|
self.__sayAll.currentContext, |
513 |
0 |
speechserver.SayAllContext.COMPLETED) |
514 |
0 |
self.__sayAll = None |
515 |
|
|
516 |
0 |
rerun = True |
517 |
|
|
518 |
0 |
self.__gidleLock.acquire() |
519 |
0 |
if self.__eventQueue.empty(): |
520 |
0 |
self.__gidleId = 0 |
521 |
0 |
rerun = False # destroy and don't call again |
522 |
0 |
self.__gidleLock.release() |
523 |
|
|
524 |
0 |
return rerun |
525 |
|
|
526 |
1 |
def notify(self, type, id, offset): |
527 |
|
"""Called by GNOME Speech when the GNOME Speech driver generates |
528 |
|
a callback. This is for internal use only. |
529 |
|
|
530 |
|
Arguments: |
531 |
|
- type: one of GNOME.Speech.speech_callback_speech_started, |
532 |
|
GNOME.Speech.speech_callback_speech_progress, |
533 |
|
GNOME.Speech.speech_callback_speech_ended |
534 |
|
- id: the id of the utterance (returned by say) |
535 |
|
- offset: the character offset into the utterance (for progress) |
536 |
|
""" |
537 |
|
|
538 |
0 |
if type == GNOME.Speech.speech_callback_speech_started: |
539 |
0 |
self.__isSpeaking = True |
540 |
0 |
elif type == GNOME.Speech.speech_callback_speech_progress: |
541 |
0 |
self.__isSpeaking = True |
542 |
0 |
elif (type == GNOME.Speech.speech_callback_speech_ended) \ |
543 |
|
and (not self.__sayAll): |
544 |
0 |
self.__isSpeaking = False |
545 |
|
|
546 |
0 |
if self.__sayAll: |
547 |
0 |
self.__gidleLock.acquire() |
548 |
0 |
self.__eventQueue.put((id, type, offset)) |
549 |
0 |
if not self.__gidleId: |
550 |
0 |
if settings.gilSleepTime: |
551 |
0 |
time.sleep(settings.gilSleepTime) |
552 |
0 |
self.__gidleId = gobject.idle_add(self.__idleHandler) |
553 |
0 |
self.__gidleLock.release() |
554 |
|
|
555 |
1 |
def getInfo(self): |
556 |
|
"""Returns [driverName, serverId] |
557 |
|
""" |
558 |
0 |
return [self.__driverName, self.__iid] |
559 |
|
|
560 |
1 |
def getVoiceFamilies(self): |
561 |
|
"""Returns a list of speechserver.VoiceFamily instances |
562 |
|
representing all the voice families known by the speech server. |
563 |
|
""" |
564 |
|
|
565 |
0 |
families = [] |
566 |
0 |
try: |
567 |
0 |
for voice in self.__driver.getAllVoices(): |
568 |
0 |
props = { |
569 |
|
speechserver.VoiceFamily.NAME : voice.name, |
570 |
|
speechserver.VoiceFamily.LOCALE : voice.language |
571 |
|
} |
572 |
0 |
families.append(speechserver.VoiceFamily(props)) |
573 |
0 |
except: |
574 |
0 |
debug.printException(debug.LEVEL_SEVERE) |
575 |
0 |
pass |
576 |
|
|
577 |
0 |
return families |
578 |
|
|
579 |
1 |
def queueText(self, text="", acss=None): |
580 |
|
"""Adds the text to the queue. |
581 |
|
|
582 |
|
Arguments: |
583 |
|
- text: text to be spoken |
584 |
|
- acss: acss.ACSS instance; if None, |
585 |
|
the default voice settings will be used. |
586 |
|
Otherwise, the acss settings will be |
587 |
|
used to augment/override the default |
588 |
|
voice settings. |
589 |
|
|
590 |
|
Output is produced by the next call to speak. |
591 |
|
""" |
592 |
0 |
self.speak(text, acss) |
593 |
|
|
594 |
1 |
def queueTone(self, pitch=440, duration=50): |
595 |
|
"""Adds a tone to the queue. |
596 |
|
|
597 |
|
Output is produced by the next call to speak. |
598 |
|
""" |
599 |
|
pass |
600 |
|
|
601 |
1 |
def queueSilence(self, duration=50): |
602 |
|
"""Adds silence to the queue. |
603 |
|
|
604 |
|
Output is produced by the next call to speak. |
605 |
|
""" |
606 |
|
pass |
607 |
|
|
608 |
1 |
def speakCharacter(self, character, acss=None): |
609 |
|
"""Speaks a single character immediately. |
610 |
|
|
611 |
|
Arguments: |
612 |
|
- character: text to be spoken |
613 |
|
- acss: acss.ACSS instance; if None, |
614 |
|
the default voice settings will be used. |
615 |
|
Otherwise, the acss settings will be |
616 |
|
used to augment/override the default |
617 |
|
voice settings. |
618 |
|
""" |
619 |
0 |
self.speak(character, acss) |
620 |
|
|
621 |
1 |
def speakUtterances(self, list, acss=None, interrupt=True): |
622 |
|
"""Speaks the given list of utterances immediately. |
623 |
|
|
624 |
|
Arguments: |
625 |
|
- list: list of strings to be spoken |
626 |
|
- acss: acss.ACSS instance; if None, |
627 |
|
the default voice settings will be used. |
628 |
|
Otherwise, the acss settings will be |
629 |
|
used to augment/override the default |
630 |
|
voice settings. |
631 |
|
- interrupt: if True, stop any speech currently in progress. |
632 |
|
""" |
633 |
0 |
i = 0 |
634 |
0 |
for text in list: |
635 |
0 |
if len(text): |
636 |
0 |
self.speak(text, acss, interrupt and (i == 0)) |
637 |
0 |
i += 1 |
638 |
|
|
639 |
1 |
def __addVerbalizedPunctuation(self, oldText): |
640 |
|
"""Depending upon the users verbalized punctuation setting, |
641 |
|
adjust punctuation symbols in the given text to their pronounced |
642 |
|
equivalents. The pronounced text will either replace the |
643 |
|
punctuation symbol or be inserted before it. In the latter case, |
644 |
|
this is to retain spoken prosity. |
645 |
|
|
646 |
|
If we are moving around by single characters, then always speak |
647 |
|
the punctuation. We try to detect this by looking for just a |
648 |
|
single character being spoken. |
649 |
|
|
650 |
|
Arguments: |
651 |
|
- oldText: text to be parsed for punctuation. |
652 |
|
|
653 |
|
Returns a text string with the punctuation symbols adjusted accordingly. |
654 |
|
""" |
655 |
|
|
656 |
|
## Replace ellipses (both manual and UTF-8) with "dot dot dot" |
657 |
|
## |
658 |
0 |
oldText = oldText.replace("...", _(" dot dot dot"), 1) |
659 |
0 |
oldText = oldText.replace("\342\200\246", _(" dot dot dot"), 1) |
660 |
0 |
oldText = oldText.decode("UTF-8") |
661 |
|
|
662 |
|
## Don't speak newlines unless the user is moving through text |
663 |
|
## using a right or left arrow key. |
664 |
|
## |
665 |
0 |
removeNewLines = True |
666 |
0 |
if orca_state.lastInputEvent and \ |
667 |
|
orca_state.lastInputEvent.__dict__.has_key("event_string"): |
668 |
0 |
lastKey = orca_state.lastInputEvent.event_string |
669 |
0 |
if lastKey == "Left" or lastKey == "Right": |
670 |
0 |
removeNewLines = False |
671 |
|
|
672 |
0 |
if removeNewLines: |
673 |
0 |
oldText = oldText.replace("\n", "", 1) |
674 |
|
|
675 |
0 |
style = settings.verbalizePunctuationStyle |
676 |
0 |
newText = '' |
677 |
0 |
for i in range(0, len(oldText)): |
678 |
0 |
try: |
679 |
0 |
level, action = \ |
680 |
|
punctuation_settings.getPunctuationInfo(oldText[i]) |
681 |
|
|
682 |
|
# Special case for punctuation in text like filenames or URL's: |
683 |
|
# |
684 |
0 |
isPrev = isNext = isSpecial = False |
685 |
0 |
if i > 0: |
686 |
0 |
isPrev = not (oldText[i - 1] in string.whitespace) |
687 |
0 |
if i < (len(oldText) - 1): |
688 |
0 |
isNext = not (oldText[i + 1] in string.whitespace) |
689 |
|
|
690 |
|
# If this is a period and there is a non-space character |
691 |
|
# on either side of it, then always speak it. |
692 |
|
# |
693 |
0 |
isSpecial = isPrev and isNext and (oldText[i] == ".") |
694 |
|
|
695 |
|
# If this is a dash and the users punctuation level is not |
696 |
|
# NONE and the previous character is a white space character, |
697 |
|
# and the next character is a dollar sign or a digit, then |
698 |
|
# always speak it. See bug #392939. |
699 |
|
# |
700 |
0 |
prevCharMatches = nextCharMatches = False |
701 |
0 |
if orca_state.activeScript: |
702 |
0 |
currencySymbols = \ |
703 |
|
orca_state.activeScript.getUnicodeCurrencySymbols() |
704 |
0 |
if i > 0: |
705 |
0 |
prevCharMatches = (oldText[i - 1] in string.whitespace) |
706 |
0 |
if i < (len(oldText) - 1): |
707 |
0 |
nextCharMatches = (oldText[i + 1] in string.digits or \ |
708 |
|
oldText[i + 1] in currencySymbols) |
709 |
|
|
710 |
0 |
if oldText[i] == "-" and \ |
711 |
|
style != settings.PUNCTUATION_STYLE_NONE and \ |
712 |
|
prevCharMatches and nextCharMatches: |
713 |
0 |
if isPrev: |
714 |
0 |
newText += " " |
715 |
0 |
newText += _("minus") |
716 |
0 |
elif (len(oldText) == 1) or isSpecial or (style <= level): |
717 |
0 |
if isPrev: |
718 |
0 |
newText += " " |
719 |
0 |
newText += chnames.getCharacterName(oldText[i]) |
720 |
0 |
if (action == punctuation_settings.PUNCTUATION_INSERT) \ |
721 |
|
and not isNext: |
722 |
0 |
newText += oldText[i].encode("UTF-8") |
723 |
0 |
if isNext: |
724 |
0 |
newText += " " |
725 |
|
else: |
726 |
0 |
newText += oldText[i].encode("UTF-8") |
727 |
0 |
except: |
728 |
0 |
if (len(oldText) == 1): |
729 |
0 |
newText += chnames.getCharacterName(oldText[i]) |
730 |
|
else: |
731 |
0 |
newText += oldText[i].encode("UTF-8") |
732 |
|
|
733 |
0 |
return newText |
734 |
|
|
735 |
1 |
def __speak(self, text=None, acss=None, interrupt=True): |
736 |
|
"""Speaks all queued text immediately. If text is not None, |
737 |
|
it is added to the queue before speaking. |
738 |
|
|
739 |
|
Arguments: |
740 |
|
- text: optional text to add to the queue before speaking |
741 |
|
- acss: acss.ACSS instance; if None, |
742 |
|
the default voice settings will be used. |
743 |
|
Otherwise, the acss settings will be |
744 |
|
used to augment/override the default |
745 |
|
voice settings. |
746 |
|
- interrupt: if True, stops any speech in progress before |
747 |
|
speaking the text |
748 |
|
|
749 |
|
Returns an id of the thing being spoken or -1 if nothing is to |
750 |
|
be spoken. |
751 |
|
""" |
752 |
|
|
753 |
|
# If the user has speech turned off, just return. |
754 |
|
# |
755 |
0 |
if not settings.enableSpeech: |
756 |
0 |
return -1 |
757 |
|
|
758 |
0 |
speaker = self.__getSpeaker(acss) |
759 |
0 |
if acss and not acss.has_key(ACSS.RATE): |
760 |
0 |
voices = settings.voices |
761 |
0 |
defaultACSS = voices[settings.DEFAULT_VOICE] |
762 |
0 |
if defaultACSS.has_key(ACSS.RATE): |
763 |
0 |
self.__setRate(speaker, defaultACSS[ACSS.RATE]) |
764 |
|
|
765 |
0 |
if not text: |
766 |
0 |
if interrupt: |
767 |
0 |
speech.stop() |
768 |
0 |
return -1 |
769 |
|
|
770 |
0 |
text = self.__addVerbalizedPunctuation(text) |
771 |
0 |
if orca_state.activeScript: |
772 |
0 |
text = orca_state.activeScript.adjustForPronunciation(text) |
773 |
|
|
774 |
0 |
try: |
775 |
|
# [[[TODO: WDW - back this stop out for now. The problem is |
776 |
|
# that we end up clipping too much speech, especially in the |
777 |
|
# case where we want to speak the contents of a popup before |
778 |
|
# speaking the object with focus.]]] |
779 |
|
# |
780 |
|
#if interrupt: |
781 |
|
# speaker.stop() |
782 |
0 |
self.__lastText = [text, acss] |
783 |
0 |
self.__isSpeaking = True |
784 |
0 |
return speaker.say(text) |
785 |
0 |
except: |
786 |
|
# On failure, remember what we said, reset our connection to the |
787 |
|
# speech synthesis driver, and try to say it again. |
788 |
|
# |
789 |
0 |
debug.printException(debug.LEVEL_SEVERE) |
790 |
0 |
debug.println(debug.LEVEL_SEVERE, "Restarting speech...") |
791 |
0 |
self.reset() |
792 |
0 |
return -1 |
793 |
|
|
794 |
1 |
def speak(self, text=None, acss=None, interrupt=True): |
795 |
|
"""Speaks all queued text immediately. If text is not None, |
796 |
|
it is added to the queue before speaking. |
797 |
|
|
798 |
|
Arguments: |
799 |
|
- text: optional text to add to the queue before speaking |
800 |
|
- acss: acss.ACSS instance; if None, |
801 |
|
the default voice settings will be used. |
802 |
|
Otherwise, the acss settings will be |
803 |
|
used to augment/override the default |
804 |
|
voice settings. |
805 |
|
- interrupt: if True, stops any speech in progress before |
806 |
|
speaking the text |
807 |
|
""" |
808 |
0 |
if self.__sayAll: |
809 |
0 |
self.stop() |
810 |
|
|
811 |
0 |
self.__speak(text, acss, interrupt) |
812 |
|
|
813 |
1 |
def isSpeaking(self): |
814 |
|
""""Returns True if the system is currently speaking.""" |
815 |
0 |
return self.__isSpeaking |
816 |
|
|
817 |
1 |
def sayAll(self, utteranceIterator, progressCallback): |
818 |
|
"""Iterates through the given utteranceIterator, speaking |
819 |
|
each utterance one at a time. |
820 |
|
|
821 |
|
Arguments: |
822 |
|
- utteranceIterator: iterator/generator whose next() function |
823 |
|
returns a new string to be spoken |
824 |
|
- progressCallback: called as progress is made |
825 |
|
""" |
826 |
0 |
try: |
827 |
0 |
[context, acss] = utteranceIterator.next() |
828 |
0 |
debug.println(debug.LEVEL_INFO, |
829 |
0 |
"SPEECH OUTPUT: '" + context.utterance + "'") |
830 |
0 |
log.info("'%s'" % context.utterance) |
831 |
0 |
self.__sayAll = _SayAll(utteranceIterator, |
832 |
0 |
context, |
833 |
0 |
self.__speak(context.utterance, acss), |
834 |
0 |
progressCallback) |
835 |
0 |
except StopIteration: |
836 |
0 |
pass |
837 |
|
|
838 |
1 |
def increaseSpeechPitch(self, step=0.5): |
839 |
|
"""Increases the speech pitch for the default voice. |
840 |
|
|
841 |
|
Arguments: |
842 |
|
- step: the pitch step increment. |
843 |
|
""" |
844 |
|
|
845 |
|
# [[[TODO: richb - this is a hack for now. Need to take min/max |
846 |
|
# values in account, plus also need to take into account that |
847 |
|
# different engines provide different pitch ranges.]]] |
848 |
|
|
849 |
0 |
voices = settings.voices |
850 |
0 |
acss = voices[settings.DEFAULT_VOICE] |
851 |
0 |
speaker = self.__getSpeaker(acss) |
852 |
|
|
853 |
0 |
pitchDelta = settings.speechPitchDelta |
854 |
|
|
855 |
0 |
try: |
856 |
0 |
pitch = min(10, self.__getPitch(speaker) + pitchDelta) |
857 |
0 |
acss[ACSS.AVERAGE_PITCH] = pitch |
858 |
0 |
self.__setPitch(speaker, pitch) |
859 |
0 |
debug.println(debug.LEVEL_CONFIGURATION, |
860 |
0 |
"speech.increaseSpeechPitch: pitch is now " \ |
861 |
|
" %d" % pitch) |
862 |
0 |
self.speak(_("higher.")) |
863 |
0 |
except: |
864 |
0 |
debug.printException(debug.LEVEL_SEVERE) |
865 |
|
|
866 |
1 |
def decreaseSpeechPitch(self, step=0.5): |
867 |
|
"""Decreases the speech pitch for the default voice. |
868 |
|
|
869 |
|
Arguments: |
870 |
|
- step: the pitch step decrement. |
871 |
|
""" |
872 |
|
|
873 |
|
# [[[TODO: WDW - this is a hack for now. Need to take min/max |
874 |
|
# values in account, plus also need to take into account that |
875 |
|
# different engines provide different rate ranges.]]] |
876 |
|
|
877 |
0 |
voices = settings.voices |
878 |
0 |
acss = voices[settings.DEFAULT_VOICE] |
879 |
0 |
speaker = self.__getSpeaker(acss) |
880 |
|
|
881 |
0 |
pitchDelta = settings.speechPitchDelta |
882 |
|
|
883 |
0 |
try: |
884 |
0 |
pitch = max(1, self.__getPitch(speaker) - pitchDelta) |
885 |
0 |
acss[ACSS.AVERAGE_PITCH] = pitch |
886 |
0 |
self.__setPitch(speaker, pitch) |
887 |
0 |
debug.println(debug.LEVEL_CONFIGURATION, |
888 |
0 |
"speech.decreaseSpeechPitch: pitch is now " \ |
889 |
|
" %d" % pitch) |
890 |
0 |
self.speak(_("lower.")) |
891 |
0 |
except: |
892 |
0 |
debug.printException(debug.LEVEL_SEVERE) |
893 |
|
|
894 |
1 |
def increaseSpeechRate(self, step=5): |
895 |
|
"""Increases the speech rate. |
896 |
|
|
897 |
|
[[[TODO: WDW - this is a hack for now. Need to take min/max |
898 |
|
values in account, plus also need to take into account that |
899 |
|
different engines provide different rate ranges.]]] |
900 |
|
""" |
901 |
|
|
902 |
0 |
voices = settings.voices |
903 |
0 |
acss = voices[settings.DEFAULT_VOICE] |
904 |
0 |
speaker = self.__getSpeaker(acss) |
905 |
|
|
906 |
0 |
rateDelta = settings.speechRateDelta |
907 |
|
|
908 |
0 |
try: |
909 |
0 |
rate = min(100, self.__getRate(speaker) + rateDelta) |
910 |
0 |
acss[ACSS.RATE] = rate |
911 |
0 |
self.__setRate(speaker, rate) |
912 |
0 |
debug.println(debug.LEVEL_CONFIGURATION, |
913 |
0 |
"speech.increaseSpeechRate: rate is now " \ |
914 |
|
" %d" % rate) |
915 |
0 |
self.speak(_("faster.")) |
916 |
0 |
except: |
917 |
0 |
debug.printException(debug.LEVEL_SEVERE) |
918 |
|
|
919 |
1 |
def decreaseSpeechRate(self, step=5): |
920 |
|
"""Decreases the rate of speech for the given ACSS. If |
921 |
|
acssName is None, the rate decrease will be applied to all |
922 |
|
known ACSSs. |
923 |
|
|
924 |
|
[[[TODO: WDW - this is a hack for now. Need to take min/max |
925 |
|
values in account, plus also need to take into account that |
926 |
|
different engines provide different rate ranges.]]] |
927 |
|
|
928 |
|
Arguments: |
929 |
|
-acssName: the ACSS whose speech rate should be decreased |
930 |
|
""" |
931 |
|
|
932 |
0 |
voices = settings.voices |
933 |
0 |
acss = voices[settings.DEFAULT_VOICE] |
934 |
0 |
speaker = self.__getSpeaker(acss) |
935 |
|
|
936 |
0 |
rateDelta = settings.speechRateDelta |
937 |
|
|
938 |
0 |
try: |
939 |
0 |
rate = max(1, self.__getRate(speaker) - rateDelta) |
940 |
0 |
acss[ACSS.RATE] = rate |
941 |
0 |
self.__setRate(speaker, rate) |
942 |
0 |
debug.println(debug.LEVEL_CONFIGURATION, |
943 |
0 |
"speech.decreaseSpeechRate: rate is now " \ |
944 |
|
" %d" % rate) |
945 |
0 |
self.speak(_("slower.")) |
946 |
0 |
except: |
947 |
0 |
debug.printException(debug.LEVEL_SEVERE) |
948 |
|
|
949 |
1 |
def stop(self): |
950 |
|
"""Stops ongoing speech and flushes the queue.""" |
951 |
0 |
if self.__sayAll: |
952 |
0 |
self.__sayAll.progressCallback( |
953 |
|
self.__sayAll.currentContext, |
954 |
0 |
speechserver.SayAllContext.INTERRUPTED) |
955 |
0 |
self.__sayAll = None |
956 |
0 |
for name in self.__speakers.keys(): |
957 |
0 |
try: |
958 |
0 |
self.__speakers[name].stop() |
959 |
0 |
except: |
960 |
0 |
pass |
961 |
0 |
self.__isSpeaking = False |
962 |
|
|
963 |
1 |
def shutdown(self): |
964 |
|
"""Shuts down the speech engine.""" |
965 |
0 |
if SpeechServer.__activeServers.has_key(self.__iid): |
966 |
0 |
for speaker in self.__speakers.values(): |
967 |
0 |
speaker.stop() |
968 |
0 |
speaker.unref() |
969 |
0 |
self.__speakers = {} |
970 |
0 |
try: |
971 |
0 |
self.__driver.unref() |
972 |
0 |
except: |
973 |
0 |
pass |
974 |
0 |
self.__driver = None |
975 |
0 |
del SpeechServer.__activeServers[self.__iid] |
976 |
|
|
977 |
1 |
def reset(self, text=None, acss=None): |
978 |
|
"""Resets the speech engine.""" |
979 |
|
|
980 |
|
# We might get into a vicious loop of resetting speech, so |
981 |
|
# we will abort if we see this happening. |
982 |
|
# |
983 |
0 |
if (time.time() - self.__lastResetTime) < 20: |
984 |
0 |
debug.println(debug.LEVEL_SEVERE, |
985 |
0 |
"Something looks wrong with speech. Aborting.") |
986 |
0 |
debug.printStack(debug.LEVEL_ALL) |
987 |
0 |
os._exit(50) |
988 |
|
else: |
989 |
0 |
self.__lastResetTime = time.time() |
990 |
|
|
991 |
0 |
speakers = self.__speakers |
992 |
0 |
self.shutdown() |
993 |
|
|
994 |
0 |
servers = bonobo.activation.query( |
995 |
|
"repo_ids.has('IDL:GNOME/Speech/SynthesisDriver:0.3')") |
996 |
|
|
997 |
0 |
for server in servers: |
998 |
0 |
if server.iid == self.__iid: |
999 |
0 |
try: |
1000 |
0 |
self.__driver = self.__activateDriver(self.__iid) |
1001 |
0 |
self.__speakers = {} |
1002 |
0 |
for name in speakers.keys(): |
1003 |
0 |
self.__getSpeaker(speakers[name]) |
1004 |
0 |
if text: |
1005 |
0 |
self.speak(text, acss) |
1006 |
0 |
break |
1007 |
0 |
except: |
1008 |
0 |
debug.printException(debug.LEVEL_SEVERE) |
1009 |
0 |
self.__driver = None |
1010 |
0 |
pass |