{ "info": { "author": "Richard Tier", "author_email": "rikatee@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "# Voice Command Lifecycle #\n\n[![code-climate-image]][code-climate]\n[![circle-ci-image]][circle-ci]\n[![codecov-image]][codecov]\n\n**Python library to manage the life-cycle of voice commands. Useful working with Alexa Voice Service.**\n\n---\n\n## Installation ##\n```bash\npip install command_lifecycle\n```\n\n### Wakeword detector ###\nA wakeword is a specific word that triggers the code to spring into action. It allows your code to be idle until the specific word is uttered.\n\nThe audio lifecycle uses [snowboy](https://github.com/Kitt-AI/snowboy#compile-a-python-wrapper) to determine if the wakeword was uttered. The library will need to be installed first.\n\nOnce you have compiled snowboy, copy the compiled `snowboy` folder to the top level of you project. By default, the folder structure should be:\n\n```\n.\n\u251c\u2500\u2500 ...\n\u251c\u2500\u2500 snowboy\n| \u251c\u2500\u2500 snowboy-detect-swig.cc\n| \u251c\u2500\u2500 snowboydetect.py\n| \u2514\u2500\u2500 resources\n| \u251c\u2500\u2500 alexa.umdl\n| \u2514\u2500\u2500 common.res\n\u2514\u2500\u2500 ...\n```\n\nIf the default structure does not suit your needs can customize the [wakeword detector](#wakeword).\n\n## Usage ##\n\nYou should send a steady stream of audio to to the lifecycle by repetitively calling `lifecycle.extend_audio(some_audio_bytes)`. If the wakeword such as \"Alexa\" (default), or \"ok, Google\" was uttered then `handle_command_started` is called. `handle_command_finised` is then called once the command audio that followed the wakeword has finished.\n\n### Microphone audio ###\n\n```py\nimport pyaudio\n\nimport command_lifecycle\n\n\nclass AudioLifecycle(command_lifecycle.BaseAudioLifecycle):\n\n def handle_command_started(self, wakeword_name):\n super().handle_command_started(wakeword_name)\n print(f'The audio contained {wakeword_name}!')\n\n def handle_command_finised(self):\n super().handle_command_finised()\n print('The command has finished')\n\nlifecycle = AudioLifecycle()\n\np = pyaudio.PyAudio()\nstream = p.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)\n\ntry:\n print('listening. Start by saying \"Alexa\". Press CTRL + C to exit.')\n while True:\n lifecycle.extend_audio(stream.read(1024))\nfinally:\n stream.stop_stream()\n stream.close()\n p.terminate()\n```\n\n\n### File audio ###\n\n```py\nimport wave\n\nimport command_lifecycle\n\nclass AudioLifecycle(command_lifecycle.BaseAudioLifecycle):\n def handle_command_started(self, wakeword_name):\n super().handle_command_started(wakeword_name)\n print(f'The audio contained {wakeword_name}!')\n\n\nlifecycle = AudioLifecycle()\nwith wave.open('./tests/resources/alexa_what_time_is_it.wav', 'rb') as f:\n while f.tell() < f.getnframes():\n lifecycle.extend_audio(f.readframes(1024))\n\nprint('The command has finished')\n\n```\n\n### Usage with Alexa ###\n\n`command_lifecycle` is useful for interacting with voice services. The lifecycle waits until a wakeword was issued and then start streaming the audio command to the voice service (using [Alexa Voice Service Client](https://github.com/richtier/alexa-voice-service-client)), then do something useful with the response:\n\n```py\nfrom avs_client.avs_client.client import AlexaVoiceServiceClient\nimport pyaudio\n\nimport command_lifecycle\n\n\nclass AudioLifecycle(command_lifecycle.BaseAudioLifecycle):\n alexa_client = AlexaVoiceServiceClient(\n client_id='my-client-id'\n secret='my-secret',\n refresh_token='my-refresh-token',\n )\n\n def __init__(self):\n self.alexa_client.connect()\n super().__init__()\n\n def handle_command_started(self, wakeword_name):\n super().handle_command_started(wakeword_name)\n audio_file = command_lifecycle.to_audio_file()\n for directive in self.alexa_client.send_audio_file(audio_file):\n # do something with the AVS audio response, e.g., play it.\n\nlifecycle = AudioLifecycle()\n\np = pyaudio.PyAudio()\nstream = p.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)\n\ntry:\n print('listening. Start by saying \"Alexa\". Press CTRL + C to exit.')\n while True:\n lifecycle.extend_audio(stream.read(1024))\nfinally:\n stream.stop_stream()\n stream.close()\n p.terminate()\n```\n\n## Customization ##\n\n### Wakeword ###\n\nThe default wakeword is \"Alexa\". This can be changed by sub-classing `command_lifecycle.wakeword.SnowboyWakewordDetector`:\n\n```py\n\nfrom command_lifecycle import wakeword\n\n\nclass MySnowboyWakewordDetector(wakeword.SnowboyWakewordDetector):\n decoder_models = [\n {\n 'name': 'CUSTOM',\n 'model': b'path/to/custom-wakeword-model.umdl'\n 'sensitivity': b'0.5',\n }\n ]\n\n\nclass AudioLifecycle(lifecycle.BaseAudioLifecycle):\n audio_detector_class = MySnowboyWakewordDetector\n\n def handle_command_started(self, wakeword_name):\n super().handle_command_started(wakeword_name)\n print(f'The audio contained the {wakeword_name}!')\n\n def handle_command_finised(self):\n super().handle_command_finised()\n print('The command has finished')\n\nlifecycle = AudioLifecycle()\n\n# now load the audio into lifecycle\n\n```\n\nSee the [Snowboy docs](https://github.com/Kitt-AI/snowboy#hotword-as-a-service) for steps on creating custom wakeword models.\n\n\n### Multiple Wakewords ###\n\nTriggering different behaviour for different wakeword may be desirable. To do this use multiple items in `decoder_models`:\n\n```py\nfrom command_lifecycle import wakeword\n\n\nclass MyMultipleWakewordDetector(wakeword.SnowboyWakewordDetector):\n GOOGLE = 'GOOGLE'\n\n decoder_models = wakeword.SnowboyWakewordDetector.decoder_models + [\n {\n 'name': GOOGLE,\n 'model': b'path/to/okgoogle.umdl',\n 'sensitivity': b'0.5',\n }\n ]\n\n\nclass AudioLifecycle(lifecycle.BaseAudioLifecycle):\n audio_detector_class = MyMultipleWakewordDetector\n\n def handle_command_started(self, wakeword_name):\n if wakeword_name == self.audio_detector.ALEXA:\n print('Alexa standing by')\n elif wakeword_name == self.audio_detector.GOOGLE:\n print('Google at your service')\n super().handle_command_started(wakeword_name)\n```\n\nYou can download wakewords from [here](https://snowboy.kitt.ai/dashboard).\n\n### Wakeword detector ###\n\nSnowboy is the default wakeword detector. Other wakeword detectors can be used by sub-classing `command_lifecycle.wakeword.BaseWakewordDetector` and setting `wakeword_detector_class` to your custom class:\n\n\n```py\nimport wave\n\nfrom command_lifecycle import lifecycle, wakeword\n\n\nclass MyCustomWakewordDetector(wakeword.BaseWakewordDetector):\n import_error_message = 'Cannot import wakeword library!'\n wakeword_library_import_path = 'path.to.wakeword.Library'\n\n def was_wakeword_uttered(self, buffer):\n # use the library to check if the audio in the buffer has the wakeword.\n # not `buffer.get()` returns the audio inside the buffer.\n ...\n\n def is_talking(self, buffer):\n # use the library to check if the audio in the buffer has audible words\n # not `buffer.get()` returns the audio inside the buffer.\n ...\n\n\nclass AudioLifecycle(lifecycle.BaseAudioLifecycle):\n audio_detector_class = MyCustomWakewordDetector\n ...\n\n\nlifecycle = AudioLifecycle()\n# now load the audio into lifecycle\n\n```\n\n\n### Handling input data ###\n\nThree input data formats are supported:\n\n| Converter | Notes |\n| ------------------------------| ---------------------------------------------------------|\n| `NoOperationConverter` | **default** Input data is already wav bytes. |\n| `WavIntSamplestoWavConverter` | Input data is list of integers. |\n| `WebAudioToWavConverter` | Input data is list of floats generated by a web browser. |\n\nCustomize this by setting the lifecycle's `audio_converter_class`:\n\n```\n\nfrom command_lifecycle.helpers import WebAudioToWavConverter\n\nclass AudioLifecycle(lifecycle.BaseAudioLifecycle):\n audio_converter_class = WebAudioToWavConverter\n\n```\n\n\n### Expecting slower or faster commands ###\n\nThe person giving the audio command might take a moment to collect their thoughts before finishing the command. This silence could be interpreted as the command ending, resulting in `handle_command_finised` being called prematurely.\n\nTo avoid this the lifecycle tolerates some silence in the command before the lifecycle timesout the command. This silence can happen at the beginning or middle of the command. Note a side-effect of this is there will be a pause between when the person has stopped talking and when `handle_command_finised` is called.\n\nTo change this default behaviour `timeout_manager_class` can be changed. The available timeout managers are:\n\n| Timeout manager | Notes |\n| ---------------------------| ------------------------------------------------ |\n| `ShortTimeoutManager` | Allows one second of silence. |\n| `MediumTimeoutManager` | **default** Allows 2 seconds of silence. |\n| `LongTimeoutManager` | Allows three seconds of silence. |\n\nTo make a custom timeout manager create a subclass of `command_lifecycle.timeout.BaseTimeoutManager`:\n\n```py\n\nimport wave\n\nfrom command_lifecycle import timeout, wakeword\n\n\nclass MyCustomTimeoutManager(timeout.BaseTimeoutManager):\n allowed_silent_seconds = 4\n\n\nclass AudioLifecycle(lifecycle.BaseAudioLifecycle):\n timeout_manager_class = MyCustomTimeoutManager\n\n```\n\n## Unit test ##\n\nTo run the unit tests, call the following commands:\n\n```sh\nmake test_requirements\nmake test\n```\n\n## Versioning\n\nWe use [SemVer](http://semver.org/) for versioning. For the versions available, see the [PyPI](https://pypi.org/project/command-lifecycle/#history).\n\n## Other projects ##\nThis library is used by [alexa-browser-client](https://github.com/richtier/alexa-browser-client), which allows you to talk to Alexa from your browser.\n\n\n[code-climate-image]: https://codeclimate.com/github/richtier/voice-command-lifecycle/badges/gpa.svg\n[code-climate]: https://codeclimate.com/github/richtier/voice-command-lifecycle\n\n[circle-ci-image]: https://circleci.com/gh/richtier/voice-command-lifecycle/tree/master.svg?style=svg\n[circle-ci]: https://circleci.com/gh/richtier/voice-command-lifecycle/tree/master\n\n[codecov-image]: https://codecov.io/gh/richtier/voice-command-lifecycle/branch/master/graph/badge.svg\n[codecov]: https://codecov.io/gh/richtier/voice-command-lifecycle\n\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/richtier/voice-command-lifecycle", "keywords": "", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "command-lifecycle", "package_url": "https://pypi.org/project/command-lifecycle/", "platform": "", "project_url": "https://pypi.org/project/command-lifecycle/", "project_urls": { "Homepage": "https://github.com/richtier/voice-command-lifecycle" }, "release_url": "https://pypi.org/project/command-lifecycle/4.1.0/", "requires_dist": [ "resettabletimer (<1.0.0,>=0.6.3)", "pytest (==3.2.3) ; extra == 'test'", "pytest-cov (==2.5.1) ; extra == 'test'", "pytest-sugar (==0.9.0) ; extra == 'test'", "flake8 (==3.4.0) ; extra == 'test'", "codecov (==2.0.9) ; extra == 'test'", "twine (<2.0.0,>=1.11.0) ; extra == 'test'", "wheel (<1.0.0,>=0.31.0) ; extra == 'test'", "setuptools (<39.0.0,>=38.6.0) ; extra == 'test'" ], "requires_python": "", "summary": "Python library to manage the life-cycle of voice commands.", "version": "4.1.0" }, "last_serial": 4678810, "releases": { "0.4.0": [ { "comment_text": "", "digests": { "md5": "54cf47858c096f67ac022c9573eeaf61", "sha256": "36914701598a63039130a58b918f4892d8d50aaf753b310d7f2c3c12e8efd71d" }, "downloads": -1, "filename": "command_lifecycle-0.4.0-py3-none-any.whl", "has_sig": false, "md5_digest": "54cf47858c096f67ac022c9573eeaf61", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 11069, "upload_time": "2017-08-26T21:29:43", "url": "https://files.pythonhosted.org/packages/a2/34/5765a78d48fb7853353c6fbb5a00c3d64dd3c8097d5d7c6be2ff42975935/command_lifecycle-0.4.0-py3-none-any.whl" } ], "0.5.0": [ { "comment_text": "", "digests": { "md5": "8cc972ae64cc30577defd7875b1241a3", "sha256": "c35f9b7d7f403ce1fda657c9ab27229a69b3a3aaff0a1efd2acf65b792afc028" }, "downloads": -1, "filename": "command_lifecycle-0.5.0-py3-none-any.whl", "has_sig": false, "md5_digest": "8cc972ae64cc30577defd7875b1241a3", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 11505, "upload_time": "2017-08-30T21:47:17", "url": "https://files.pythonhosted.org/packages/7b/4b/7d6ca6e6ac22e4b7303e88ee0e5f999959779d41b83b5cfbbd5172450d33/command_lifecycle-0.5.0-py3-none-any.whl" } ], "1.0.0": [ { "comment_text": "", "digests": { "md5": "3a5ebf6678f592dc51d6ca634d245f5f", "sha256": "2bb00eb4beae1b11ca6ec81ead2da0eaaa86a440c186507cac369229a5e06f76" }, "downloads": -1, "filename": "command_lifecycle-1.0.0-py3-none-any.whl", "has_sig": false, "md5_digest": "3a5ebf6678f592dc51d6ca634d245f5f", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 12307, "upload_time": "2017-10-09T22:04:08", "url": "https://files.pythonhosted.org/packages/82/b8/c0d7ca4ab0da4717204e9442b594b3c6c06e7eccb9e672982cb7cef0d4fb/command_lifecycle-1.0.0-py3-none-any.whl" } ], "2.0.0": [ { "comment_text": "", "digests": { "md5": "16879a8f37a07307f8870ee25f64d9f8", "sha256": "89b5c8cf282bf2e120dc6f87dfb8c26fe47a6020df00a9f2a0d87dbee4c29a00" }, "downloads": -1, "filename": "command_lifecycle-2.0.0-py3-none-any.whl", "has_sig": false, "md5_digest": "16879a8f37a07307f8870ee25f64d9f8", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 12481, "upload_time": "2018-07-28T22:42:32", "url": "https://files.pythonhosted.org/packages/83/e4/1dd2d39bd92373a6a17d6ae4a83fc3277a3e455138c9ae8bf0d0eaddea10/command_lifecycle-2.0.0-py3-none-any.whl" } ], "2.1.2": [ { "comment_text": "", "digests": { "md5": "8530d6eab084f765154c2fb5d6ebac90", "sha256": "7c7ee8ae28dd407f71ecbfda562e62b8d54c5c16997f44dd78254ea2ae9360b8" }, "downloads": -1, "filename": "command_lifecycle-2.1.2-py3-none-any.whl", "has_sig": false, "md5_digest": "8530d6eab084f765154c2fb5d6ebac90", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 12994, "upload_time": "2018-09-13T21:20:09", "url": "https://files.pythonhosted.org/packages/6a/b4/882e51282ee5262dee4ef440ddcf99fd43a6638e1aecdd6ec048beb18318/command_lifecycle-2.1.2-py3-none-any.whl" } ], "2.1.6": [ { "comment_text": "", "digests": { "md5": "56761604f00ce80ac162983b368312d4", "sha256": "3b6db81db94b3f24fe1647400b1891ff6c367a2d6749a6d14c1bfd4affcd4d6e" }, "downloads": -1, "filename": "command_lifecycle-2.1.6-py3-none-any.whl", "has_sig": false, "md5_digest": "56761604f00ce80ac162983b368312d4", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 8603, "upload_time": "2018-09-13T21:30:05", "url": "https://files.pythonhosted.org/packages/2c/e3/01354b7e674dccad2974a5252ecf2748b12482a3d9c8ed4c2bee6e772d4e/command_lifecycle-2.1.6-py3-none-any.whl" } ], "4.0.0": [ { "comment_text": "", "digests": { "md5": "abb258a9fa33a9ceadba189a0600abb5", "sha256": "2656a3a2d8ae64dbf60850fc7737893d72d41c3d564dfcb26f05b5a3700471a7" }, "downloads": -1, "filename": "command_lifecycle-4.0.0-py3-none-any.whl", "has_sig": false, "md5_digest": "abb258a9fa33a9ceadba189a0600abb5", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 9440, "upload_time": "2019-01-01T19:36:07", "url": "https://files.pythonhosted.org/packages/cd/d7/cc4e38ad3f4880bf94ada9c3095ab6b008ab0b0489e94d14b5721fb81468/command_lifecycle-4.0.0-py3-none-any.whl" } ], "4.1.0": [ { "comment_text": "", "digests": { "md5": "79954435f6cc6e48a7f459d6384bf1f7", "sha256": "9e1a613770ec8952cf1e5b4d97e906b8fb44e99e39aa5ea397b0e44f1d6e9d5c" }, "downloads": -1, "filename": "command_lifecycle-4.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "79954435f6cc6e48a7f459d6384bf1f7", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 9159, "upload_time": "2019-01-09T23:11:30", "url": "https://files.pythonhosted.org/packages/0c/41/8413d6d35fa226ae1e60ea6a330f36ad9b6d2d3b922d7c9dcd291b7fc209/command_lifecycle-4.1.0-py3-none-any.whl" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "79954435f6cc6e48a7f459d6384bf1f7", "sha256": "9e1a613770ec8952cf1e5b4d97e906b8fb44e99e39aa5ea397b0e44f1d6e9d5c" }, "downloads": -1, "filename": "command_lifecycle-4.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "79954435f6cc6e48a7f459d6384bf1f7", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 9159, "upload_time": "2019-01-09T23:11:30", "url": "https://files.pythonhosted.org/packages/0c/41/8413d6d35fa226ae1e60ea6a330f36ad9b6d2d3b922d7c9dcd291b7fc209/command_lifecycle-4.1.0-py3-none-any.whl" } ] }