diff --git a/examples/speech/buildozer.spec b/examples/speech/buildozer.spec new file mode 100644 index 000000000..4b231a853 --- /dev/null +++ b/examples/speech/buildozer.spec @@ -0,0 +1,193 @@ +[app] + +# (str) Title of your application +title = Plyer Speech Recognition + +# (str) Package name +package.name = plyer.speech.recognition + +# (str) Package domain (needed for android/ios packaging) +package.domain = org.test + +# (str) Source code where the main.py live +source.dir = . + +# (list) Source files to include (let empty to include all the files) +source.include_exts = py,png,jpg,kv,atlas + +# (list) Source files to exclude (let empty to not exclude anything) +#source.exclude_exts = spec + +# (list) List of directory to exclude (let empty to not exclude anything) +#source.exclude_dirs = tests, bin + +# (list) List of exclusions using pattern matching +#source.exclude_patterns = license,images/*/*.jpg + +# (str) Application versioning (method 1) +# version.regex = __version__ = ['"](.*)['"] +# version.filename = %(source.dir)s/main.py + +# (str) Application versioning (method 2) +version = 1.0 + +# (list) Application requirements +# comma seperated e.g. requirements = sqlite3,kivy +requirements = kivy,plyer + +# (str) Custom source folders for requirements +# Sets custom source for any requirements with recipes +# requirements.source.kivy = ../../kivy + +# (list) Garden requirements +#garden_requirements = + +# (str) Presplash of the application +#presplash.filename = %(source.dir)s/data/presplash.png + +# (str) Icon of the application +#icon.filename = %(source.dir)s/data/icon.png + +# (str) Supported orientation (one of landscape, portrait or all) +orientation = portrait + +# (bool) Indicate if the application should be fullscreen or not +fullscreen = 1 + + +# +# Android specific +# + +# (list) Permissions +android.permissions = INTERNET,RECORD_AUDIO + +# (int) Android API to use +#android.api = 14 + +# (int) Minimum API required (8 = Android 2.2 devices) +#android.minapi = 8 + +# (int) Android SDK version to use +#android.sdk = 21 + +# (str) Android NDK version to use +#android.ndk = 9c + +# (bool) Use --private data storage (True) or --dir public storage (False) +#android.private_storage = True + +# (str) Android NDK directory (if empty, it will be automatically downloaded.) +#android.ndk_path = + +# (str) Android SDK directory (if empty, it will be automatically downloaded.) +#android.sdk_path = + +# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) +#android.p4a_dir = + +# (list) python-for-android whitelist +#android.p4a_whitelist = + +# (str) Android entry point, default is ok for Kivy-based app +#android.entrypoint = org.renpy.android.PythonActivity + +# (list) List of Java .jar files to add to the libs so that pyjnius can access +# their classes. Don't add jars that you do not need, since extra jars can slow +# down the build process. Allows wildcards matching, for example: +# OUYA-ODK/libs/*.jar +#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar + +# (list) List of Java files to add to the android project (can be java or a +# directory containing the files) +#android.add_src = + +# (str) python-for-android branch to use, if not master, useful to try +# not yet merged features. +#android.branch = master + +# (str) OUYA Console category. Should be one of GAME or APP +# If you leave this blank, OUYA support will not be enabled +#android.ouya.category = GAME + +# (str) Filename of OUYA Console icon. It must be a 732x412 png image. +#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png + +# (str) XML file to include as an intent filters in tag +#android.manifest.intent_filters = + +# (list) Android additionnal libraries to copy into libs/armeabi +#android.add_libs_armeabi = libs/android/*.so +#android.add_libs_armeabi_v7a = libs/android-v7/*.so +#android.add_libs_x86 = libs/android-x86/*.so +#android.add_libs_mips = libs/android-mips/*.so + +# (bool) Indicate whether the screen should stay on +# Don't forget to add the WAKE_LOCK permission if you set this to True +#android.wakelock = False + +# (list) Android application meta-data to set (key=value format) +#android.meta_data = + +# (list) Android library project to add (will be added in the +# project.properties automatically.) +#android.library_references = + +# +# iOS specific +# + +# (str) Name of the certificate to use for signing the debug version +# Get a list of available identities: buildozer ios list_identities +#ios.codesign.debug = "iPhone Developer: ()" + +# (str) Name of the certificate to use for signing the release version +#ios.codesign.release = %(ios.codesign.debug)s + + +[buildozer] + +# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) +log_level = 2 + +# (int) Display warning if buildozer is run as root (0 = False, 1 = True) +warn_on_root = 1 + + +# ----------------------------------------------------------------------------- +# List as sections +# +# You can define all the "list" as [section:key]. +# Each line will be considered as a option to the list. +# Let's take [app] / source.exclude_patterns. +# Instead of doing: +# +#[app] +#source.exclude_patterns = license,data/audio/*.wav,data/images/original/* +# +# This can be translated into: +# +#[app:source.exclude_patterns] +#license +#data/audio/*.wav +#data/images/original/* +# + + +# ----------------------------------------------------------------------------- +# Profiles +# +# You can extend section / key with a profile +# For example, you want to deploy a demo version of your application without +# HD content. You could first change the title to add "(demo)" in the name +# and extend the excluded directories to remove the HD content. +# +#[app@demo] +#title = My Application (demo) +# +#[app:source.exclude_patterns@demo] +#images/hd/* +# +# Then, invoke the command line with the "demo" profile: +# +#buildozer --profile demo android debug diff --git a/examples/speech/main.py b/examples/speech/main.py new file mode 100644 index 000000000..ee791bd2d --- /dev/null +++ b/examples/speech/main.py @@ -0,0 +1,90 @@ +from kivy.app import App +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.properties import StringProperty +from kivy.uix.boxlayout import BoxLayout + +from plyer import speech + +Builder.load_string(''' +#:import speech plyer.speech + +: + orientation: 'vertical' + Label: + size_hint_y: None + height: sp(40) + text: 'Is supported: %s' % speech.exist() + Label: + size_hint_y: None + height: sp(40) + text: 'Possible Matches' + TextInput: + id: results + hint_text: 'results (auto stop)' + size_hint_y: 0.3 + TextInput: + id: partial + hint_text: 'partial results (manual stop)' + size_hint_y: 0.3 + TextInput: + id: errors + size_hint_y: None + height: sp(20) + Button: + id: start_button + text: 'Start Listening' + on_release: + root.start_listening() + +''') + + +class SpeechInterface(BoxLayout): + '''Root Widget.''' + + def start_listening(self): + if speech.listening: + self.stop_listening() + return + + start_button = self.ids.start_button + start_button.text = 'Stop' + + self.ids.results.text = '' + self.ids.partial.text = '' + + speech.start() + + Clock.schedule_interval(self.check_state, 1 / 5) + + def stop_listening(self): + start_button = self.ids.start_button + start_button.text = 'Start Listening' + + speech.stop() + self.update() + + Clock.unschedule(self.check_state) + + def check_state(self, dt): + # if the recognizer service stops, change UI + if not speech.listening: + self.stop_listening() + + def update(self): + self.ids.partial.text = '\n'.join(speech.partial_results) + self.ids.results.text = '\n'.join(speech.results) + + +class SpeechApp(App): + + def build(self): + return SpeechInterface() + + def on_pause(self): + return True + + +if __name__ == "__main__": + SpeechApp().run() diff --git a/plyer/__init__.py b/plyer/__init__.py index 3447dccc0..bdf731b9e 100644 --- a/plyer/__init__.py +++ b/plyer/__init__.py @@ -4,13 +4,14 @@ ''' - -__all__ = ('accelerometer', 'audio', 'barometer', 'battery', 'call', 'camera', - 'compass', 'email', 'filechooser', 'flash', 'gps', 'gravity', - 'gyroscope', 'irblaster', 'light', 'orientation', 'notification', - 'proximity', 'sms', 'tts', 'uniqueid', 'vibrator', 'wifi', - 'temperature', 'humidity', 'spatialorientation', 'brightness', - 'storagepath', 'processors', 'cpu', 'bluetooth', 'screenshot') +__all__ = ( + 'accelerometer', 'audio', 'barometer', 'battery', 'bluetooth', + 'brightness', 'call', 'camera', 'compass', 'cpu', 'email', 'filechooser', + 'flash', 'gps', 'gravity', 'gyroscope', 'humidity', 'irblaster', + 'keystore', 'light', 'notification', 'orientation', 'processors', + 'proximity', 'screenshot', 'sms', 'spatialorientation', 'speech', + 'storagepath', 'temperature', 'tts', 'uniqueid', 'vibrator', 'wifi' +) __version__ = '1.3.3.dev0' @@ -76,6 +77,9 @@ #: Sms proxy to :class:`plyer.facades.Sms` sms = Proxy('sms', facades.Sms) +#: Speech proxy to :class:`plyer.facades.Speech` +speech = Proxy('speech', facades.Speech) + #: TTS proxy to :class:`plyer.facades.TTS` tts = Proxy('tts', facades.TTS) diff --git a/plyer/facades/__init__.py b/plyer/facades/__init__.py index 2b432201d..ba9262d67 100644 --- a/plyer/facades/__init__.py +++ b/plyer/facades/__init__.py @@ -11,7 +11,8 @@ 'IrBlaster', 'Light', 'Orientation', 'Notification', 'Proximity', 'Sms', 'TTS', 'UniqueID', 'Vibrator', 'Wifi', 'Flash', 'CPU', 'Temperature', 'Humidity', 'SpatialOrientation', 'Brightness', - 'Processors', 'StoragePath', 'keystore', 'Bluetooth', 'Screenshot') + 'Processors', 'StoragePath', 'keystore', 'Bluetooth', 'Screenshot', + 'Speech') from plyer.facades.accelerometer import Accelerometer from plyer.facades.audio import Audio @@ -32,6 +33,7 @@ from plyer.facades.orientation import Orientation from plyer.facades.notification import Notification from plyer.facades.sms import Sms +from plyer.facades.speech import Speech from plyer.facades.tts import TTS from plyer.facades.uniqueid import UniqueID from plyer.facades.vibrator import Vibrator diff --git a/plyer/facades/speech.py b/plyer/facades/speech.py new file mode 100644 index 000000000..f5f779a05 --- /dev/null +++ b/plyer/facades/speech.py @@ -0,0 +1,159 @@ +class Speech(object): + ''' + .. versionadded:: 1.3.3 + + Speech Recognition facade. + + In order to check that your device supports voice recognition use method + `exist`. + + Variable `language` indicates which language will be used to match words + from voice. + + Use `start` to start voice recognition immediately and `stop` to stop. + + .. note:: + Needed permissions for Android: `RECORD_AUDIO` (and `INTERNET` if you + want online voice recognition API to be used) + + .. note:: + On Android platform, after execute `start` method you can hear BEEP! + Mute sound in order to disable it. + + .. note:: + For Android implementation to work there has to be an application + with `android.speech.RecognitionService` implementation present + in the system. Mostly it's `com.google.android.googlequicksearchbox` + or "Google" application (the search bar with the launcher widget). + + Offline Speech Recognition on Android + ------------------------------------- + + Requires any application that provides an + `android.speech.RecognitionService` implementation to the other apps. One + of such applications is on a lot of devices preinstalled Google (quick + search box). + + The API prefers offline recognition, but should be able to switch to online + alternative in case you don't have a language package installed (`INTERNET` + permission necessary). + + You can enable offline speech recognition this way (Android 8.1): + + * open the `Settings` app + * choose `Language & Input` / `Language & Keyboard` (Samsung might include + it in the `General` category) + * choose `On-Screen keyboard` or `Voice search` + * choose `Google Keyboard` + * choose `Offline Speech recognition` + * download language package if you don't have one already + ''' + + _language = 'en-US' + ''' + Default language in which platform will try to recognize voice. + In order to change language pick one from list by using + `supported_languages` method. + ''' + + _supported_languages = [ + 'en-US', + 'pl-PL' + ] + + results = [] + ''' + List of sentences found while listening. It may consist of many similar + and possible sentences that was recognition program. + ''' + + errors = [] + ''' + List of errors found while listening. + ''' + + partial_results = [] + ''' + List of results found while the listener is still being active. + ''' + + prefer_offline = True + ''' + Preference whether to use offline language package necessary for + each platform dependant implementation or online API. + ''' + + listening = False + ''' + Current state of listening. + ''' + + @property + def supported_languages(self): + ''' + Return list of supported languages used in recognition. + ''' + + return self._supported_languages + + @property + def language(self): + ''' + Return current language. + ''' + + return self._language + + @language.setter + def language(self, lang): + ''' + Set current language. + + Value can not be set if it's not supported. See `supported_languages` + to get what language you can set. + + .. note:: + We obviously can't check each language, therefore if you find + that a specific language is available to you and the only limitation + is our check for the internally defined `supported_languages`, feel + free to open a pull request for adding your language to the list. + ''' + + if lang in self.supported_languages: + self._language = lang + + # public methods + def start(self): + ''' + Start listening. + ''' + + self.results = [] + self.partial_results = [] + self._start() + self.listening = True + + def stop(self): + ''' + Stop listening. + ''' + + self._stop() + self.listening = False + + def exist(self): + ''' + Returns a boolean for speech recognition availability. + ''' + + return self._exist() + + # private methods + def _start(self): + raise NotImplementedError + + def _stop(self): + raise NotImplementedError + + def _exist(self): + raise NotImplementedError diff --git a/plyer/platforms/android/speech.py b/plyer/platforms/android/speech.py new file mode 100644 index 000000000..7f7927bff --- /dev/null +++ b/plyer/platforms/android/speech.py @@ -0,0 +1,252 @@ +from android.runnable import run_on_ui_thread + +from jnius import autoclass +from jnius import java_method +from jnius import PythonJavaClass + +from plyer.facades import Speech +from plyer.platforms.android import activity + +ArrayList = autoclass('java.util.ArrayList') +Bundle = autoclass('android.os.Bundle') +Context = autoclass('android.content.Context') +Intent = autoclass('android.content.Intent') +RecognizerIntent = autoclass('android.speech.RecognizerIntent') +RecognitionListener = autoclass('android.speech.RecognitionListener') +SpeechRecognizer = autoclass('android.speech.SpeechRecognizer') + +SpeechResults = SpeechRecognizer.RESULTS_RECOGNITION + + +class SpeechListener(PythonJavaClass): + __javainterfaces__ = ['android/speech/RecognitionListener'] + + # class variables because PythonJavaClass class failed + # to see them later in getters and setters + _error_callback = None + _result_callback = None + _partial_result_callback = None + _volume_callback = None + + def __init__(self): + super(SpeechListener, self).__init__() + + # overwrite class variables in the object + self._error_callback = None + self._result_callback = None + self._partial_result_callback = None + self._volume_callback = None + + # error handling + @property + def error_callback(self): + return self._error_callback + + @error_callback.setter + def error_callback(self, callback): + ''' + Set error callback. It is called when error occurs. + + :param callback: function with one parameter for error message + ''' + + self._error_callback = callback + + # result handling + @property + def result_callback(self): + return self._result_callback + + @result_callback.setter + def result_callback(self, callback): + ''' + Set result callback. It is called when results are received. + + :param callback: function with one parameter for lists of strings + ''' + + self._result_callback = callback + + @property + def partial_result_callback(self): + return self._partial_result_callback + + @partial_result_callback.setter + def partial_result_callback(self, callback): + ''' + Set partial result callback. It is called when partial results are + received while the listener is still in listening mode. + + :param callback: function with one parameter for lists of strings + ''' + + self._partial_result_callback = callback + + # voice changes handling + @property + def volume_callback(self): + return self._volume_callback + + @volume_callback.setter + def volume_callback(self, callback): + ''' + Set volume voice callback. + + It is called when loudness of the voice changes. + + :param callback: function with one parameter for volume RMS dB (float). + ''' + self._volume_callback = callback + + # Implementation Java Interfaces + @java_method('()V') + def onBeginningOfSpeech(self): + pass + + @java_method('([B)V') + def onBufferReceived(self, buffer): + pass + + @java_method('()V') + def onEndOfSpeech(self): + pass + + @java_method('(I)V') + def onError(self, error): + msg = '' + if error == SpeechRecognizer.ERROR_AUDIO: + msg = 'audio' + if error == SpeechRecognizer.ERROR_CLIENT: + msg = 'client' + if error == SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS: + msg = 'insufficient_permissions' + if error == SpeechRecognizer.ERROR_NETWORK: + msg = 'network' + if error == SpeechRecognizer.ERROR_NETWORK_TIMEOUT: + msg = 'network_timeout' + if error == SpeechRecognizer.ERROR_NO_MATCH: + msg = 'no_match' + if error == SpeechRecognizer.ERROR_RECOGNIZER_BUSY: + msg = 'recognizer_busy' + if error == SpeechRecognizer.ERROR_SERVER: + msg = 'server' + if error == SpeechRecognizer.ERROR_SPEECH_TIMEOUT: + msg = 'speech_timeout' + + if msg and self.error_callback: + self.error_callback('error:' + msg) + + @java_method('(ILandroid/os/Bundle;)V') + def onEvent(self, event_type, params): + pass + + @java_method('(Landroid/os/Bundle;)V') + def onPartialResults(self, results): + texts = [] + matches = results.getStringArrayList(SpeechResults) + for match in matches.toArray(): + if isinstance(match, bytes): + match = match.decode('utf-8') + texts.append(match) + + if texts and self.partial_result_callback: + self.partial_result_callback(texts) + + @java_method('(Landroid/os/Bundle;)V') + def onReadyForSpeech(self, params): + pass + + @java_method('(Landroid/os/Bundle;)V') + def onResults(self, results): + texts = [] + matches = results.getStringArrayList(SpeechResults) + for match in matches.toArray(): + if isinstance(match, bytes): + match = match.decode('utf-8') + texts.append(match) + + if texts and self.result_callback: + self.result_callback(texts) + + @java_method('(F)V') + def onRmsChanged(self, rmsdB): + if self.volume_callback: + self.volume_callback(rmsdB) + + +class AndroidSpeech(Speech): + ''' + Android Speech Implementation. + + Android class `SpeechRecognizer`'s listening deactivates automatically. + + Class methods `_on_error()`, `_on_result()` listeners. You can find + documentation here: + https://developer.android.com/reference/android/speech/RecognitionListener + ''' + + def _on_error(self, msg): + self.errors.append(msg) + self.stop() + + def _on_result(self, messages): + self.results.extend(messages) + self.stop() + + def _on_partial(self, messages): + self.partial_results.extend(messages) + + @run_on_ui_thread + def _start(self): + intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) + intent.putExtra( + RecognizerIntent.EXTRA_CALLING_PACKAGE, + activity.getPackageName() + ) + + # language preferences + intent.putExtra( + RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE, self.language + ) + intent.putExtra( + RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH + ) + + # results settings + intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1000) + intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, True) + if self.prefer_offline: + intent.putExtra(RecognizerIntent.EXTRA_PREFER_OFFLINE, True) + + # listener and callbacks + listener = SpeechListener() + listener.error_callback = self._on_error + listener.result_callback = self._on_result + listener.partial_result_callback = self._on_partial + + # create recognizer and start + self.speech = SpeechRecognizer.createSpeechRecognizer(activity) + self.speech.setRecognitionListener(listener) + self.speech.startListening(intent) + + @run_on_ui_thread + def _stop(self): + if not self.speech: + return + + # stop listening + self.speech.stopListening() + + # free object + self.speech.destroy() + self.speech = None + + def _exist(self): + return bool( + SpeechRecognizer.isRecognitionAvailable(activity) + ) + + +def instance(): + return AndroidSpeech() diff --git a/plyer/platforms/linux/notification.py b/plyer/platforms/linux/notification.py index 36baa8af3..7704f7afd 100644 --- a/plyer/platforms/linux/notification.py +++ b/plyer/platforms/linux/notification.py @@ -58,7 +58,7 @@ def instance(): Instance for facade proxy. ''' try: - import dbus # pylint: disable=unused-variable + import dbus # pylint: disable=unused-variable,unused-import return NotifyDbus() except ImportError: msg = ("The Python dbus package is not installed.\n" diff --git a/plyer/platforms/linux/wifi.py b/plyer/platforms/linux/wifi.py index 28d5c3247..615f44f87 100644 --- a/plyer/platforms/linux/wifi.py +++ b/plyer/platforms/linux/wifi.py @@ -118,7 +118,7 @@ def instance(): import sys try: - import wifi # pylint: disable=unused-variable + import wifi # pylint: disable=unused-variable,unused-import except ImportError: sys.stderr.write("python-wifi not installed. try:" "`pip install --user wifi`.")