# Copyright (C) 2011 One Laptop Per Child # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import os import logging from gettext import gettext as _ import gi gi.require_version('Gtk', '3.0') from gi.repository import Gio from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GLib from gi.repository import GObject _HAS_GST = True try: gi.require_version('Gst', '1.0') from gi.repository import Gst Gst.init(None) Gst.parse_launch('espeak') except BaseException: logging.error('Gst or the espeak plugin is not installed in the system.') _HAS_GST = False from sugar3 import power DEFAULT_PITCH = 0 DEFAULT_RATE = 0 _SAVE_TIMEOUT = 500 SPEECH_SCHEMA = 'org.sugarlabs.speech' # This voice names are use dto allow the translation of the voice names. # If espeak add new voices, we need update this list. translated_espeak_voices = { # Translators: http://en.wikipedia.org/wiki/Afrikaans_language 'af': _('Afrikaans'), # Translators: http://en.wikipedia.org/wiki/Aragonese_language 'an': _('Aragonese'), # Translators: http://en.wikipedia.org/wiki/Bulgarian_language 'bg': _('Bulgarian'), # Translators: http://en.wikipedia.org/wiki/Bosnian_language 'bs': _('Bosnian'), # Translators: http://en.wikipedia.org/wiki/Catalan_language 'ca': _('Catalan'), # Translators: http://en.wikipedia.org/wiki/Czech_language 'cs': _('Czech'), # Translators: http://en.wikipedia.org/wiki/Welsh_language 'cy': _('Welsh'), # Translators: http://en.wikipedia.org/wiki/Danish_language 'da': _('Danish'), # Translators: http://en.wikipedia.org/wiki/German_language 'de': _('German'), # Translators: http://en.wikipedia.org/wiki/Modern_Greek 'el': _('Greek'), 'en': _('English'), # Translators: http://en.wikipedia.org/wiki/British_English 'en_gb': _('English Britain'), # Translators: http://en.wikipedia.org/wiki/Scottish_English 'en_sc': _('English scottish'), 'en_uk-north': _('English-north'), # Translators: http://en.wikipedia.org/wiki/Received_Pronunciation 'en_uk-rp': _('English_rp'), # Translators: http://en.wikipedia.org/wiki/West_Midlands_English 'en_uk-wmids': _('English_wmids'), # Translators: http://en.wikipedia.org/wiki/American_English 'en_us': _('English USA'), # Translators: http://en.wikipedia.org/wiki/Caribbean_English 'en_wi': _('English West Indies'), # Translators: http://en.wikipedia.org/wiki/Esperanto 'eo': _('Esperanto'), # Translators: http://en.wikipedia.org/wiki/Spanish_language 'es': _('Spanish'), 'es_la': _('Spanish latin american'), # Translators: http://en.wikipedia.org/wiki/Estonian_language 'et': _('Estonian'), # Translators: http://en.wikipedia.org/wiki/Persian_language 'fa': _('Farsi'), # Translators: http://en.wikipedia.org/wiki/Fingilish 'fa_pin': _('Farsi-pinglish'), # Translators: http://en.wikipedia.org/wiki/Finnish_language 'fi': _('Finnish'), # Translators: http://en.wikipedia.org/wiki/Belgian_French 'fr_be': _('French belgium'), # Translators: http://en.wikipedia.org/wiki/French_language 'fr_fr': _('French'), # Translators: http://en.wikipedia.org/wiki/Irish_language 'ga': _('Irish-gaeilge'), # Translators: http://en.wikipedia.org/wiki/Ancient_Greek 'grc': _('Greek-ancient'), # Translators: http://en.wikipedia.org/wiki/Hindi 'hi': _('Hindi'), # Translators: http://en.wikipedia.org/wiki/Croatian_language 'hr': _('Croatian'), # Translators: http://en.wikipedia.org/wiki/Hungarian_language 'hu': _('Hungarian'), # Translators: http://en.wikipedia.org/wiki/Armenian_language 'hy': _('Armenian'), # Translators: http://en.wikipedia.org/wiki/Western_Armenian 'hy_west': _('Armenian (west)'), # Translators: http://en.wikipedia.org/wiki/Indonesian_language 'id': _('Indonesian'), # Translators: http://en.wikipedia.org/wiki/Icelandic_language 'is': _('Icelandic'), # Translators: http://en.wikipedia.org/wiki/Italian_language 'it': _('Italian'), # Translators: http://en.wikipedia.org/wiki/Lojban 'jbo': _('Lojban'), # Translators: http://en.wikipedia.org/wiki/Georgian_language 'ka': _('Georgian'), # Translators: http://en.wikipedia.org/wiki/Kannada_language 'kn': _('Kannada'), # Translators: http://en.wikipedia.org/wiki/Kurdish_language 'ku': _('Kurdish'), # Translators: http://en.wikipedia.org/wiki/Latin 'la': _('Latin'), # Translators: http://en.wikipedia.org/wiki/Lithuanian_language 'lt': _('Lithuanian'), # Translators: http://en.wikipedia.org/wiki/Latvian_language 'lv': _('Latvian'), # Translators: http://en.wikipedia.org/wiki/Macedonian_language 'mk': _('Macedonian'), # Translators: http://en.wikipedia.org/wiki/Malayalam 'ml': _('Malayalam'), # Translators: http://en.wikipedia.org/wiki/Malay_language 'ms': _('Malay'), # Translators: http://en.wikipedia.org/wiki/Nepali_language 'ne': _('Nepali'), # Translators: http://en.wikipedia.org/wiki/Dutch_language 'nl': _('Dutch'), # Translators: http://en.wikipedia.org/wiki/Norwegian_language 'no': _('Norwegian'), # Translators: http://en.wikipedia.org/wiki/Punjabi_language 'pa': _('Punjabi'), # Translators: http://en.wikipedia.org/wiki/Polish_language 'pl': _('Polish'), # Translators: http://en.wikipedia.org/wiki/Brazilian_Portuguese 'pt_br': _('Portuguese (Brazil)'), # Translators: http://en.wikipedia.org/wiki/Portuguese_language 'pt_pt': _('Portuguese (Portugal)'), # Translators: http://en.wikipedia.org/wiki/Romanian_language 'ro': _('Romanian'), # Translators: http://en.wikipedia.org/wiki/Russian_language 'ru': _('Russian'), # Translators: http://en.wikipedia.org/wiki/Slovak_language 'sk': _('Slovak'), # Translators: http://en.wikipedia.org/wiki/Albanian_language 'sq': _('Albanian'), # Translators: http://en.wikipedia.org/wiki/Serbian_language 'sr': _('Serbian'), # Translators: http://en.wikipedia.org/wiki/Swedish_language 'sv': _('Swedish'), # Translators: http://en.wikipedia.org/wiki/Swahili_language 'sw': _('Swahili'), # Translators: http://en.wikipedia.org/wiki/Tamil_language 'ta': _('Tamil'), # Translators: http://en.wikipedia.org/wiki/Turkish_language 'tr': _('Turkish'), # Translators: http://en.wikipedia.org/wiki/Vietnamese_language 'vi': _('Vietnam'), 'vi_hue': _('Vietnam_hue'), 'vi_sgn': _('Vietnam_sgn'), # Translators: http://en.wikipedia.org/wiki/Mandarin_Chinese 'zh': _('Mandarin'), # Translators: http://en.wikipedia.org/wiki/Cantonese 'zh_yue': _('Cantonese') } class SpeechManager(GObject.GObject): __gtype_name__ = 'SpeechManager' __gsignals__ = { 'play': (GObject.SignalFlags.RUN_FIRST, None, []), 'pause': (GObject.SignalFlags.RUN_FIRST, None, []), 'stop': (GObject.SignalFlags.RUN_FIRST, None, []), 'mark': (GObject.SignalFlags.RUN_FIRST, None, [str]) } MIN_PITCH = -100 MAX_PITCH = 100 MIN_RATE = -100 MAX_RATE = 100 def __init__(self, **kwargs): GObject.GObject.__init__(self, **kwargs) self.player = None if not self.enabled(): return self.player = GstSpeechPlayer() self.player.connect('play', self._update_state, 'play') self.player.connect('stop', self._update_state, 'stop') self.player.connect('pause', self._update_state, 'pause') self.player.connect('mark', self._mark_cb) self._default_voice_name = self.player.get_default_voice() self._pitch = DEFAULT_PITCH self._rate = DEFAULT_RATE self._is_playing = False self._is_paused = False self._save_timeout_id = -1 self.restore() def enabled(self): return _HAS_GST def _update_state(self, player, signal): self._is_playing = (signal == 'play') self._is_paused = (signal == 'pause') self.emit(signal) def _mark_cb(self, player, value): self.emit('mark', value) def get_is_playing(self): return self._is_playing is_playing = GObject.Property(type=bool, getter=get_is_playing, setter=None, default=False) def get_is_paused(self): return self._is_paused is_paused = GObject.Property(type=bool, getter=get_is_paused, setter=None, default=False) def get_pitch(self): return self._pitch def get_rate(self): return self._rate def set_pitch(self, pitch): self._pitch = pitch if self._save_timeout_id != -1: GLib.source_remove(self._save_timeout_id) self._save_timeout_id = GLib.timeout_add(_SAVE_TIMEOUT, self.save) def set_rate(self, rate): self._rate = rate if self._save_timeout_id != -1: GLib.source_remove(self._save_timeout_id) self._save_timeout_id = GLib.timeout_add(_SAVE_TIMEOUT, self.save) def say_text(self, text, pitch=None, rate=None, lang_code=None): if pitch is None: pitch = self._pitch if rate is None: rate = self._rate if lang_code is None: voice_name = self._default_voice_name else: voice_name = self.player.get_all_voices()[lang_code] if text: logging.debug( 'PLAYING %r lang %r pitch %r rate %r', text, voice_name, pitch, rate) self.player.speak(pitch, rate, voice_name, text) def say_selected_text(self): clipboard = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY) clipboard.request_text(self.__primary_selection_cb, None) def pause(self): self.player.pause_sound_device() def restart(self): self.player.restart_sound_device() def stop(self): self.player.stop_sound_device() def __primary_selection_cb(self, clipboard, text, user_data): self.say_text(text) def save(self): self._save_timeout_id = -1 schema_source = Gio.SettingsSchemaSource.get_default() if schema_source.lookup(SPEECH_SCHEMA, True) is None: return False settings = Gio.Settings(SPEECH_SCHEMA) settings.set_int('pitch', self._pitch) settings.set_int('rate', self._rate) logging.debug('saving speech configuration pitch %s rate %s', self._pitch, self._rate) return False def restore(self): schema_source = Gio.SettingsSchemaSource.get_default() if schema_source.lookup(SPEECH_SCHEMA, True) is None: return settings = Gio.Settings(SPEECH_SCHEMA) self._pitch = settings.get_int('pitch') self._rate = settings.get_int('rate') logging.debug('loading speech configuration pitch %s rate %s', self._pitch, self._rate) def get_all_voices(self): if self.player: return self.player.get_all_voices() return None def get_all_traslated_voices(self): """ deprecated after 0.112, due to method name spelling error """ if self.player: return self.player.get_all_translated_voices() return None def get_all_translated_voices(self): if self.player: return self.player.get_all_translated_voices() return None class GstSpeechPlayer(GObject.GObject): __gsignals__ = { 'play': (GObject.SignalFlags.RUN_FIRST, None, []), 'pause': (GObject.SignalFlags.RUN_FIRST, None, []), 'stop': (GObject.SignalFlags.RUN_FIRST, None, []), 'mark': (GObject.SignalFlags.RUN_FIRST, None, [str]) } def __init__(self): GObject.GObject.__init__(self) self.pipeline = None self._all_voices = None self._all_translated_voices = None def restart_sound_device(self): if self.pipeline is None: logging.debug('Trying to restart not initialized sound device') return power.get_power_manager().inhibit_suspend() self.pipeline.set_state(Gst.State.PLAYING) self.emit('play') def pause_sound_device(self): if self.pipeline is None: return self.pipeline.set_state(Gst.State.PAUSED) power.get_power_manager().restore_suspend() self.emit('pause') def stop_sound_device(self): if self.pipeline is None: return self.pipeline.set_state(Gst.State.NULL) power.get_power_manager().restore_suspend() self.emit('stop') def make_pipeline(self, command): if self.pipeline is not None: self.stop_sound_device() del self.pipeline self.pipeline = Gst.parse_launch(command) bus = self.pipeline.get_bus() bus.add_signal_watch() bus.connect('message', self.__pipe_message_cb) def __pipe_message_cb(self, bus, message): if message.type in (Gst.MessageType.EOS, Gst.MessageType.ERROR): self.pipeline.set_state(Gst.State.NULL) self.pipeline = None power.get_power_manager().restore_suspend() self.emit('stop') elif message.type is Gst.MessageType.ELEMENT and \ message.get_structure().get_name() == 'espeak-mark': mark_value = message.get_structure().get_value('mark') self.emit('mark', mark_value) def speak(self, pitch, rate, voice_name, text): # TODO workaround for http://bugs.sugarlabs.org/ticket/1801 if not [i for i in text if i.isalnum()]: return self.make_pipeline('espeak name=espeak ! autoaudiosink') src = self.pipeline.get_by_name('espeak') src.props.text = text src.props.pitch = pitch src.props.rate = rate src.props.voice = voice_name src.props.track = 2 # track for marks self.restart_sound_device() def get_all_voices(self): if self._all_voices is not None: return self._all_voices self._init_voices() return self._all_voices def get_all_translated_voices(self): if self._all_translated_voices is not None: return self._all_translated_voices self._init_voices() return self._all_translated_voices def _init_voices(self): self._all_voices = {} self._all_translated_voices = {} for voice in Gst.ElementFactory.make('espeak', None).props.voices: name, language, dialect = voice if dialect != 'none': lang_code = language + '_' + dialect else: lang_code = language self._all_voices[lang_code] = name if lang_code in translated_espeak_voices: self._all_translated_voices[lang_code] = \ translated_espeak_voices[lang_code] else: self._all_translated_voices[lang_code] = name def get_default_voice(self): """Try to figure out the default voice, from the current locale ($LANG) Fall back to espeak's voice called Default.""" voices = self.get_all_voices() locale = os.environ.get('LANG', '') language_location = locale.split('.', 1)[0].lower() language = language_location.split('_')[0] # if the language is es but not es_es default to es_la (latin voice) if language == 'es' and language_location != 'es_es': language_location = 'es_la' best = voices.get(language_location) or voices.get(language) \ or 'english' return best