├── .gitignore ├── .travis.yml ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── debian ├── changelog ├── compat ├── control ├── copyright ├── files ├── install └── rules ├── docs ├── .gitignore ├── 404.html ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── _includes │ ├── footer.html │ ├── head.html │ ├── menu.html │ └── nav.html ├── _layouts │ ├── default.html │ ├── home.html │ ├── page.html │ ├── plain.html │ └── spotify-login.html ├── _posts │ ├── 2017-10-02-API.markdown │ ├── 2017-10-02-Creating-Skills.markdown │ ├── 2017-10-02-Examples.markdown │ ├── 2017-10-02-Getting-Started.markdown │ ├── 2017-10-02-Installation.markdown │ ├── 2017-10-02-Microphone-Setup.markdown │ ├── 2017-10-02-The-Snipsfile.markdown │ ├── 2017-10-02-Troubleshooting-iOS.markdown │ ├── 2017-10-02-Troubleshooting.markdown │ └── 2017-10-02-iOS-App.markdown ├── _sass │ ├── buttons.scss │ ├── colors.scss │ ├── highlight.scss │ ├── mixins.scss │ └── typography.scss ├── about.md ├── css │ ├── font-awesome │ │ ├── HELP-US-OUT.txt │ │ ├── css │ │ │ ├── font-awesome.css │ │ │ └── font-awesome.min.css │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ ├── less │ │ │ ├── animated.less │ │ │ ├── bordered-pulled.less │ │ │ ├── core.less │ │ │ ├── fixed-width.less │ │ │ ├── font-awesome.less │ │ │ ├── icons.less │ │ │ ├── larger.less │ │ │ ├── list.less │ │ │ ├── mixins.less │ │ │ ├── path.less │ │ │ ├── rotated-flipped.less │ │ │ ├── screen-reader.less │ │ │ ├── stacked.less │ │ │ └── variables.less │ │ └── scss │ │ │ ├── _animated.scss │ │ │ ├── _bordered-pulled.scss │ │ │ ├── _core.scss │ │ │ ├── _fixed-width.scss │ │ │ ├── _icons.scss │ │ │ ├── _larger.scss │ │ │ ├── _list.scss │ │ │ ├── _mixins.scss │ │ │ ├── _path.scss │ │ │ ├── _rotated-flipped.scss │ │ │ ├── _screen-reader.scss │ │ │ ├── _stacked.scss │ │ │ ├── _variables.scss │ │ │ └── font-awesome.scss │ └── main.scss ├── fonts │ ├── apercu_bold.ttf │ ├── apercu_medium.ttf │ ├── apercu_mono.ttf │ └── apercu_regular.ttf ├── images │ ├── AppStoreBadge.png │ ├── AppStoreBadge@2x.png │ ├── Cover.png │ ├── Cover@2x.png │ ├── IconApple.png │ ├── IconApple@2x.png │ ├── IconLinux.png │ ├── IconLinux@2x.png │ ├── Logo.png │ ├── Logo@2x.png │ ├── Platform.png │ ├── Platform@2x.png │ ├── Snips Spotify.png │ ├── Snips Spotify@2x.png │ ├── iPhone.png │ ├── iPhone@2x.png │ └── skills │ │ ├── Coffee.png │ │ ├── Coffee@2x.png │ │ ├── Hue.png │ │ ├── Hue@2x.png │ │ ├── Light.png │ │ ├── Light@2x.png │ │ ├── Music.png │ │ ├── Music@2x.png │ │ ├── OWM.png │ │ ├── OWM@2x.png │ │ ├── Smarter.png │ │ ├── Smarter@2x.png │ │ ├── Sonos.png │ │ ├── Sonos@2x.png │ │ ├── Weather.png │ │ └── Weather@2x.png ├── index.md └── spotify-login.md ├── setup.cfg ├── setup.py ├── snipsmanager ├── __init__.py ├── cli.py ├── commands │ ├── __init__.py │ ├── assistant │ │ ├── __init__.py │ │ ├── fetch.py │ │ └── load.py │ ├── base.py │ ├── install │ │ ├── __init__.py │ │ ├── addon.py │ │ ├── bluetooth.py │ │ ├── install.py │ │ ├── skill.py │ │ └── skills.py │ ├── run.py │ ├── scaffold.py │ ├── session │ │ ├── __init__.py │ │ ├── login.py │ │ └── logout.py │ └── setup │ │ ├── __init__.py │ │ ├── microphone.py │ │ ├── speaker.py │ │ └── systemd │ │ ├── __init__.py │ │ ├── bluetooth.py │ │ └── snipsmanager.py ├── config │ ├── __init__.py │ ├── asound.conf │ │ ├── __init__.py │ │ ├── asound.conf.default │ │ ├── asound.conf.jabra │ │ ├── asound.conf.respeaker │ │ ├── asound.conf.speakerbonnet │ │ └── asoundrc.speakerbonnet │ ├── drivers │ │ ├── __init__.py │ │ └── adafruit_bonnet.sh │ └── systemd │ │ ├── __init__.py │ │ ├── snipsble.service │ │ └── snipsmanager.service ├── models │ ├── __init__.py │ ├── dialoguedef.py │ ├── intentdef.py │ ├── notificationdef.py │ └── skilldef.py ├── snipsmanager ├── templates │ ├── LICENSE.txt │ ├── MANIFEST.in │ ├── README.rst │ ├── __init__.py │ ├── intent_registry_template.py │ ├── intent_template.py │ ├── lint.cfg │ ├── setup.cfg │ └── setup.py └── utils │ ├── __init__.py │ ├── addons.py │ ├── assistant_downloader.py │ ├── auth.py │ ├── cache.py │ ├── http_helpers.py │ ├── intent_class_generator.py │ ├── microphone_setup.py │ ├── object_from_dict.py │ ├── os_helpers.py │ ├── pip_installer.py │ ├── snips.py │ ├── snipsfile.py │ ├── speaker_setup.py │ ├── systemd.py │ └── wizard.py ├── stdeb.cfg └── tests ├── auth └── test_auth.py ├── commands └── test_install.py ├── test_cli.py └── utils └── test_wizard.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .vscode/ 6 | # C extensions 7 | *.so 8 | .idea/ 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # dotenv 58 | .env 59 | 60 | # virtualenv 61 | .venv 62 | venv/ 63 | ENV/ 64 | 65 | # Spyder project settings 66 | .spyderproject 67 | .spyproject 68 | 69 | # Rope project settings 70 | .ropeproject 71 | 72 | # mkdocs documentation 73 | /site 74 | 75 | # mypy 76 | .mypy_cache/ 77 | 78 | ./src/ 79 | temp/ 80 | .DS_Store 81 | .AppleDouble/ 82 | Makefile 83 | snipsmanager/data/music 84 | .snips 85 | test.py 86 | venv 87 | docs/build 88 | docs/node_modules 89 | docs/package-lock.json 90 | *.deb 91 | .snips_env 92 | 93 | /Snipsfile* 94 | /assistant.zip 95 | /Snipsfiles -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | cache: pip 5 | before_install: 6 | - sudo apt-get update -qq 7 | - sudo apt-get install -y libsdl-mixer1.2 libusb-1.0 python-pyaudio libsdl1.2-dev cython cython3 libudev-dev python-dev libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsmpeg-dev python-numpy libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev portaudio19-dev nodejs build-essential 8 | - pip install pytest pytest-cov 9 | - pip install coveralls 10 | install: 11 | - python setup.py install 12 | script: 13 | - python setup.py test 14 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | snipsmanager is written and maintained by Snips. 2 | 3 | Development Lead 4 | ~~~~~~~~~~~~~~~~ 5 | 6 | - Anthony Reinette 7 | - Michael Fester 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@snips.ai. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to Contribute 2 | ================= 3 | 4 | Contributions are welcome! Not familiar with the codebase yet? No problem! 5 | There are many ways to contribute to open source projects: reporting bugs, 6 | helping with the documentation, spreading the word and of course, adding 7 | new features and patches. 8 | 9 | Getting Started 10 | --------------- 11 | * Make sure you have a GitHub account. 12 | * Open a [new issue](https://github.com/snipsco/snipsmanager/issues), assuming one does not already exist. 13 | * Clearly describe the issue including steps to reproduce when it is a bug. 14 | 15 | Making Changes 16 | -------------- 17 | * Fork this repository. 18 | * Create a feature branch from where you want to base your work. 19 | * Make commits of logical units (if needed rebase your feature branch before 20 | submitting it). 21 | * Check for unnecessary whitespace with ``git diff --check`` before committing. 22 | * Make sure your commit messages are well formatted. 23 | * If your commit fixes an open issue, reference it in the commit message (f.e. `#15`). 24 | * Run all the tests (if existing) to assure nothing else was accidentally broken. 25 | 26 | These guidelines also apply when helping with documentation. 27 | 28 | Submitting Changes 29 | ------------------ 30 | * Push your changes to a feature branch in your fork of the repository. 31 | * Submit a `Pull Request`. 32 | * Wait for maintainer feedback. 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Snips 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE.txt 3 | include setup.cfg 4 | include snipsmanager/config/* 5 | include snipsmanager/config/asound.conf/* 6 | include snipsmanager/config/drivers/* 7 | include snipsmanager/config/systemd/* 8 | include snipsmanager/templates/* 9 | include snipsmanager/snipsmanager 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snips Manager [❗️DEPRECATED] 2 | 3 | [![Build Status](https://travis-ci.org/snipsco/snipsmanager.svg)](https://travis-ci.org/snipsco/snipsmanager) 4 | [![PyPi](https://img.shields.io/pypi/v/snipsmanager.svg)](https://pypi.python.org/pypi/snipsmanager) 5 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/snipsco/snipsmanager/master/LICENSE.txt) 6 | 7 | > ❗️ The SnipsManager project is now deprecated. We now offer a more native way to run intents actions with the `snips-skills-server`. 8 | > A migration guide can be found [here](https://snips.gitbook.io/tutorials/t/technical-guides/snipsmanager-migration-guide). 9 | 10 | ## Notice of Deprecation 11 | 12 | As of 30/04/2018, the `snipsmanager` project is now deprecated as the snips platform now offers a more native way to run skills **actions**. 13 | All skills written with `snipsmanager` can easily be ported to the `snips-skills-server`. It is also possible to run `snipsmanager` within `snips-skills-server`. 14 | 15 | A migration guide can be found [here](https://snips.gitbook.io/tutorials/t/technical-guides/snipsmanager-migration-guide). 16 | 17 | 18 | ## Introduction 19 | 20 | The Snips Manager is a tool for easily setting up and managing a [Snips](https://www.snips.ai) assistant. 21 | 22 | A single configuration file, the [Snipsfile](https://github.com/snipsco/snipsmanager/wiki/The-Snipsfile), is required to create a Snips assistant. In it, you specify: 23 | 24 | - The URL of your assistant model, as created in the [Snips Console](https://console.snips.ai) 25 | - The [lambdas](https://github.com/snipsco/snipsmanager/wiki/Creating-a-Lambda) you want to install 26 | - Bindings between intents and lambdas 27 | - If required, additional parameters for your lambdas, such as an API key or the address of a lamp 28 | - Various configuration parameters, such as language and logging preferences. 29 | 30 | Check out [Awesome Snips](https://github.com/snipsco/awesome-snips), a curated list of Snips assistants, lambdas and other resources to get you started. 31 | 32 | ## Installation 33 | 34 | ### Debian package 35 | 36 | Snips Manager is available as an `apt-get` package. To install it, run the following: 37 | 38 | ```sh 39 | $ sudo apt-get update 40 | $ sudo apt-get install -y dirmngr 41 | $ sudo bash -c 'echo "deb https://raspbian.snips.ai/$(lsb_release -cs) stable main" > /etc/apt/sources.list.d/snips.list' 42 | $ sudo apt-key adv --keyserver pgp.mit.edu --recv-keys D4F50CDCA10A2849 43 | $ sudo apt-get update 44 | $ sudo apt-get install -y snipsmanager 45 | ``` 46 | 47 | ### Python package 48 | 49 | Snips Manager also comes as a `pip` package. This however requires installing a few dependencies beforehand. Start by running: 50 | 51 | ```sh 52 | $ sudo apt-get update 53 | $ sudo apt-get install git python-pip libsdl-mixer1.2 libusb-1.0 \ 54 | python-pyaudio libsdl1.2-dev cython cython3 libudev-dev \ 55 | python-dev libsdl-image1.2-dev libsdl-mixer1.2-dev \ 56 | libsdl-ttf2.0-dev libsmpeg-dev python-numpy libportmidi-dev \ 57 | libswscale-dev libavformat-dev libavcodec-dev \ 58 | portaudio19-dev nodejs build-essential -y 59 | ``` 60 | 61 | Next, create a Python virtual environment to avoid conflicts with existing dependencies, and to be able to run Snips Manager without root privileges: 62 | 63 | ```sh 64 | $ sudo pip install --upgrade virtualenv 65 | $ virtualenv --python=/usr/bin/python2.7 snips 66 | $ source snips/bin/activate 67 | (snips) $ pip install pip --upgrade 68 | ``` 69 | 70 | You may replace `snips` with any name for your virtual environment. 71 | 72 | We are ready to install the `snipsmanager` package: 73 | 74 | ```sh 75 | (snips) $ pip install snipsmanager 76 | ``` 77 | 78 | ## macOS 79 | 80 | On macOS, Snips Manager is also available as a `pip` package. To install, Portaudio, Pyaudio and SDL are needed: 81 | 82 | ```sh 83 | $ sudo easy_install pip 84 | $ brew install portaudio 85 | $ brew install sdl 86 | $ pip install --global-option='build_ext' \ 87 | --global-option='-I/usr/local/include' \ 88 | --global-option='-L/usr/local/lib' pyaudio 89 | ``` 90 | 91 | Next, like with Raspbian, we create a Python virtual environment in which Snips Manager will be run: 92 | 93 | ```sh 94 | $ sudo pip install --upgrade virtualenv 95 | $ virtualenv --python=/usr/bin/python2.7 snips 96 | $ source snips/bin/activate 97 | (snips) $ pip install pip --upgrade 98 | ``` 99 | 100 | Snips Manager can now be installed: 101 | 102 | ```sh 103 | (snips) $ pip install snipsmanager 104 | ``` 105 | 106 | ## Usage 107 | 108 | ### Creating the Snipsfile 109 | 110 | Start your project by creating a `Snipsfile`, which is where all the configuration is set. This is a simple text file, adhering to the [YAML](https://en.wikipedia.org/wiki/YAML) format. Here is a basic configuration: 111 | 112 | ```yaml 113 | assistant_url: 114 | default_location: Paris,fr 115 | skills: 116 | - package_name: snipshue 117 | class_name: SnipsHue 118 | url: https://github.com/snipsco/snips-skill-hue 119 | params: 120 | hostname: 121 | username: 122 | light_ids: [1, 2, 3, 4, 5, 6] 123 | intents: 124 | - intent: ActivateLightColor 125 | action: "turn_on" 126 | - intent: DeactivateObject 127 | action: "turn_off" 128 | ``` 129 | 130 | For further explanations and examples, check out our [Snipsfile Wiki](https://github.com/snipsco/snipsmanager/wiki/The-Snipsfile). 131 | 132 | ### Installing the lambdas 133 | 134 | Next, setup the assistant by running the `install` command: 135 | 136 | ```sh 137 | $ snipsmanager install 138 | ``` 139 | 140 | The `snipsmanager` service will automatically start on boot. You can also start it manually by running: 141 | 142 | ```sh 143 | $ snipsmanager run 144 | ``` 145 | 146 | ## Contributing 147 | 148 | Please see the [Contribution Guidelines](https://github.com/snipsco/snipsmanager/blob/master/CONTRIBUTING.md). 149 | 150 | ## Copyright 151 | 152 | This library is provided by [Snips](https://www.snips.ai) as Open Source software. See [LICENSE.txt](https://github.com/snipsco/snipsmanager/blob/master/LICENSE.txt) for more information. 153 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | snipsmanager (0.2.0.0) unstable; urgency=medium 2 | 3 | * Initial debian packaging 4 | 5 | -- Snips Labs Tue, 12 Sep 2017 10:36:35 +0200 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: snipsmanager 2 | Section: contrib/python 3 | Priority: extra 4 | Maintainer: Snips 5 | Build-Depends: debhelper (>= 9), python, python-dev, dh-virtualenv (>= 0.10), tar, python2.7, libsdl-mixer1.2, libusb-1.0-0, libusb-1.0-0-dev, python-pyaudio, libsdl1.2-dev, libudev-dev, python-dev, libsdl-image1.2-dev, libsdl-mixer1.2-dev, libsdl-ttf2.0-dev, libsmpeg-dev, python-numpy, libportmidi-dev, libswscale-dev, libavformat-dev, libavcodec-dev, portaudio19-dev, nodejs, build-essential, libyaml-dev, libpython2.7-dev, python-yaml, git 6 | Standards-Version: 3.9.5 7 | Homepage: www.snips.ai 8 | 9 | Package: snipsmanager 10 | Architecture: any 11 | Pre-Depends: dpkg (>= 1.16.1), python2.7, ${misc:Pre-Depends} 12 | Depends: ${python:Depends},libusb-1.0-0, libsdl1.2-dev, portaudio19-dev, libsdl-mixer1.2, ${misc:Depends}, git 13 | Description: A Python package and its dependencies packaged up as DEB in an isolated virtualenv. 14 | . 15 | This is a distribution of "snipsmanager" as a self-contained 16 | Python virtualenv wrapped into a Debian package ("omnibus" package, 17 | all passengers on board). The packaged virtualenv is kept in sync with 18 | the host's interpreter automatically. 19 | . 20 | See https://github.com/spotify/dh-virtualenv for more details. 21 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Snips 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /debian/files: -------------------------------------------------------------------------------- 1 | snipsmanager_0.2.0.0_armhf.deb contrib/python extra 2 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | snipsmanager/snipsmanager /usr/bin 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # 3 | # Build Debian package using https://github.com/spotify/dh-virtualenv 4 | # 5 | # The below targets create a clean copy of the workdir via 6 | # using "sdist", else "pip" goes haywire when installing from 7 | # sourcedir ".", because that includes the debian build stage, 8 | # and a recursive explosion ensues when symlinks are followed. 9 | # 10 | # It also ensures your MANIFEST is complete and at least covers 11 | # all files needed for a release build. 12 | 13 | # Increase trace logging, see debhelper(7) (uncomment to enable) 14 | #DH_VERBOSE=1 15 | 16 | export DH_VIRTUALENV_INSTALL_ROOT=/opt/venvs 17 | SNAKE=/usr/bin/python2 18 | EXTRA_REQUIREMENTS=--preinstall "setuptools>=17.1,<34" --preinstall "pip>=9" --preinstall "wheel" --preinstall "no-manylinux1" 19 | DH_VENV_ARGS=--with python-virtualenv --setuptools --python $(SNAKE) $(EXTRA_REQUIREMENTS) #-v 20 | PACKAGE=$(shell dh_listpackages) 21 | VERSION=$(shell $(SNAKE) setup.py --version) 22 | SDIST_DIR=debian/$(PACKAGE)-$(VERSION) 23 | 24 | clean: 25 | test ! -d dist || rm -rf dist 26 | test ! -d $(SDIST_DIR) || rm -rf $(SDIST_DIR) 27 | dh $@ $(DH_VENV_ARGS) 28 | 29 | build-arch: 30 | $(SNAKE) setup.py sdist --formats tar 31 | mkdir -p $(SDIST_DIR) 32 | tar -x -C $(SDIST_DIR) --strip-components=1 --exclude '*.egg-info' -f dist/*.tar 33 | dh $@ $(DH_VENV_ARGS) --sourcedir $(SDIST_DIR) 34 | 35 | %: 36 | dh $@ $(DH_VENV_ARGS) --sourcedir $(SDIST_DIR) 37 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-metadata 4 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | 18 | 19 |
20 |

404

21 | 22 |

Page not found :(

23 |

The requested page could not be found.

24 |
25 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Hello! This is where you manage which Jekyll version is used to run. 4 | # When you want to use a different version, change it below, save the 5 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 6 | # 7 | # bundle exec jekyll serve 8 | # 9 | # This will help ensure the proper Jekyll version is running. 10 | # Happy Jekylling! 11 | gem "jekyll", "3.5.2" 12 | 13 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 14 | gem "minima", "~> 2.0" 15 | 16 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 17 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 18 | # gem "github-pages", group: :jekyll_plugins 19 | 20 | # If you have any plugins, put them here! 21 | group :jekyll_plugins do 22 | gem "jekyll-feed", "~> 0.6" 23 | end 24 | 25 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 26 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 27 | 28 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.5.2) 5 | public_suffix (>= 2.0.2, < 4.0) 6 | colorator (1.1.0) 7 | ffi (1.9.18) 8 | forwardable-extended (2.6.0) 9 | jekyll (3.5.2) 10 | addressable (~> 2.4) 11 | colorator (~> 1.0) 12 | jekyll-sass-converter (~> 1.0) 13 | jekyll-watch (~> 1.1) 14 | kramdown (~> 1.3) 15 | liquid (~> 4.0) 16 | mercenary (~> 0.3.3) 17 | pathutil (~> 0.9) 18 | rouge (~> 1.7) 19 | safe_yaml (~> 1.0) 20 | jekyll-feed (0.9.2) 21 | jekyll (~> 3.3) 22 | jekyll-sass-converter (1.5.0) 23 | sass (~> 3.4) 24 | jekyll-watch (1.5.0) 25 | listen (~> 3.0, < 3.1) 26 | kramdown (1.15.0) 27 | liquid (4.0.0) 28 | listen (3.0.8) 29 | rb-fsevent (~> 0.9, >= 0.9.4) 30 | rb-inotify (~> 0.9, >= 0.9.7) 31 | mercenary (0.3.6) 32 | minima (2.1.1) 33 | jekyll (~> 3.3) 34 | pathutil (0.14.0) 35 | forwardable-extended (~> 2.6) 36 | public_suffix (3.0.0) 37 | rb-fsevent (0.10.2) 38 | rb-inotify (0.9.10) 39 | ffi (>= 0.5.0, < 2) 40 | rouge (1.11.1) 41 | safe_yaml (1.0.4) 42 | sass (3.5.1) 43 | sass-listen (~> 4.0.0) 44 | sass-listen (4.0.0) 45 | rb-fsevent (~> 0.9, >= 0.9.4) 46 | rb-inotify (~> 0.9, >= 0.9.7) 47 | 48 | PLATFORMS 49 | ruby 50 | 51 | DEPENDENCIES 52 | jekyll (= 3.5.2) 53 | jekyll-feed (~> 0.6) 54 | minima (~> 2.0) 55 | tzinfo-data 56 | 57 | BUNDLED WITH 58 | 1.15.4 59 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Jekyll! 2 | # 3 | # This config file is meant for settings that affect your whole blog, values 4 | # which you are expected to set up once and rarely edit after that. If you find 5 | # yourself editing this file very often, consider using Jekyll's data files 6 | # feature for the data you need to update frequently. 7 | # 8 | # For technical reasons, this file is *NOT* reloaded automatically when you use 9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process. 10 | 11 | # Site settings 12 | # These are used to personalize your new site. If you look in the HTML files, 13 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. 14 | # You can create any custom variable you would like, and they will be accessible 15 | # in the templates via {{ site.myvariable }}. 16 | title: Snips Skills Manager 17 | email: labs@snips.ai 18 | description: > # this means to ignore newlines until "baseurl:" 19 | Write an awesome description for your new site here. You can edit this 20 | line in _config.yml. It will appear in your document head meta (for 21 | Google search results) and in your feed.xml site description. 22 | baseurl: "/snipsskills" # the subpath of your site, e.g. /blog 23 | url: "https://www.snips.ai" # the base hostname & protocol for your site, e.g. http://example.com 24 | twitter_username: snips 25 | github_username: snipsco 26 | 27 | # Build settings 28 | 29 | markdown: kramdown 30 | kramdown: 31 | input: GFM 32 | hard_wrap: false 33 | auto_ids: true 34 | syntax_highlighter: rouge 35 | 36 | # theme: minima 37 | # plugins: 38 | # - jekyll-feed 39 | # Exclude from processing. 40 | # The following items will not be processed, by default. Create a custom list 41 | # to override the default setting. 42 | # exclude: 43 | # - Gemfile 44 | # - Gemfile.lock 45 | # - node_modules 46 | # - vendor/bundle/ 47 | # - vendor/cache/ 48 | # - vendor/gems/ 49 | # - vendor/ruby/ 50 | -------------------------------------------------------------------------------- /docs/_includes/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{page.title }} 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/_includes/menu.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_includes/nav.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 | {% include menu.html %} 6 |
7 |
8 | {{ content }} 9 |
10 |
11 | {% include footer.html %} 12 | 13 | -------------------------------------------------------------------------------- /docs/_layouts/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 | {% include menu.html %} 6 |
7 |
8 |

Create private voice assistants without coding

9 | 10 |
11 | 12 |
13 |
14 |
15 |

16 | 17 | apt-get install snipsskills* 18 |

19 | 23 |

*You'll need to add deb https://s3.amazonaws.com/snips-deb/ stable main to /etc/apt/sources.list.d/snips.list, and run apt-get update.

24 |
25 |
26 | 27 |
28 |
29 |

30 | Read the Getting Started Guide, or head over to the Examples page and start building your own Raspberry Pi assistant!

31 |

Also check out the awesome-snips page for sample projects, tutorials and various resources to get you quickly up to speed. 32 |

33 |
34 |
35 | {% include footer.html %} 36 | 37 | -------------------------------------------------------------------------------- /docs/_layouts/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 | {% include menu.html %} 6 |
7 |
8 | {% include nav.html %} 9 |
10 |

{{ page.title }}

11 | {{ content }} 12 |
13 |
14 |
15 | {% include footer.html %} 16 | 17 | -------------------------------------------------------------------------------- /docs/_layouts/plain.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 |
6 | {{ content }} 7 |
8 | 9 | -------------------------------------------------------------------------------- /docs/_layouts/spotify-login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 | {% include menu.html %} 6 |
7 |
8 | Login with Spotify 9 | 10 |
11 |
12 | {% include footer.html %} 13 | 56 | 57 | -------------------------------------------------------------------------------- /docs/_posts/2017-10-02-API.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "API" 4 | date: 2017-10-02 12:53:25 +0200 5 | permalink: /api/ 6 | --- 7 | 8 | ## Fetching an assistant 9 | 10 | ```sh 11 | snipsskills fetch assistant 12 | ``` 13 | 14 | Downloads an assistant to the device, either from the Snips Console, or from a publicly available location. 15 | 16 | Parameters: 17 | 18 | * `--id` (optional): the assistant console ID, included in the URL of your assistant in the console. 19 | * `--url` (optional): if not using the console ID, you may specify a public URL pointing to your assistant.zip file 20 | * `--snipsfile` (optional): alternatively, you may point to a local Snipsfile, from which to extract the ID or URL of the assistant. In this case, the Snipsfile should contain an items with key `assistant_id`, or `assistant_url`, for instance: 21 | 22 | ```yaml 23 | ... 24 | assistant_id: proj_123ABC 25 | ... 26 | ``` 27 | 28 | * `--force_download` (optional): if true, the command will force redownloading the assistant. Otherwise, it will look for it in the cache. 29 | 30 | 31 | ## Loading an assistant 32 | 33 | ```sh 34 | snipsskills load assistant 35 | ``` 36 | 37 | Loads an assistant into the system, making it available to the Snips Platform, and generating necessary intent classes for the Snips Skills server. 38 | 39 | Parameters: 40 | 41 | * `--file` (optional): location of a local `assistant.zip` file. If not provided, Snips Skills will revert to the defualt location for the assistant. 42 | * `--platform_only` (optional): if true, only the Snips Platform will be updated, bypassing the intent class generation required for the Snips Skills server. 43 | -------------------------------------------------------------------------------- /docs/_posts/2017-10-02-Creating-Skills.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Creating Skills" 4 | date: 2017-10-02 12:53:25 +0200 5 | permalink: /creating-skills/ 6 | --- 7 | 8 | ## Creating skills -------------------------------------------------------------------------------- /docs/_posts/2017-10-02-Examples.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Examples" 4 | date: 2017-10-02 12:53:25 +0200 5 | permalink: /examples/ 6 | --- 7 | 8 | Here are a few sample skills to get you started. If you want to add yours, simply submit a [Pull Request on Github](https://github.com/snipsco/snips-skills-registry/pulls), adding the appropriate line to the [Registry](https://github.com/snipsco/snips-skills-registry/blob/master/REGISTRY) file. This will also make it discoverable through the [iOS App]({{ site.baseurl }}/{% post_url 2017-10-02-iOS-App %}). 9 | 10 |
11 | 12 |

Philips Hue

13 |

Control your Philips Hue lights

14 | 17 |
18 | 19 |
20 | 21 |

Sonos

22 |

Play music on a Sonos system

23 | 27 |
28 | 29 |
30 | 31 |

Smarter Coffee

32 |

Brew coffee with the Smarter Coffee machine

33 | 36 |
37 | 38 |
39 | 40 |

Weather Forecasts

41 |

Get weather forecasts using the OpenWeatherMap API

42 | 45 |
46 | -------------------------------------------------------------------------------- /docs/_posts/2017-10-02-Getting-Started.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Getting Started" 4 | date: 2017-10-02 12:53:25 +0200 5 | permalink: /getting-started/ 6 | --- 7 | 8 | In this guide, we will help you through the process of creating a simple assistant using the Snips Assistant Manager. 9 | 10 | ## What is a Snips assistant composed of? 11 | 12 | A typical voice assistant is a device, such as a Raspberry Pi, equipped with a microphone. The device is waiting for a hotword, e.g. *Hey Snips*, *OK Google* or *Alexa*, that puts it in listening mode. Then it starts transcribing the user's voice into text, using Automatic Speech Recognition (ASR). After a short silence, it sends the transcribed phrase to a Natural Language Understanding (NLU) component, which extract the meaning of the phrase encoded in a JSON object, a so-called *intent*, that the system can understand and act upon. For instance, if the user says *Turn on the lights*, the system will output an intent which could be named `Lights` with parameter `state=on`. This is the first part illustrated in the following diagram, and consistutes the Snips Platform: 13 | 14 | 15 | 16 | The second part, the Skills part, corresponds to what happens after an intent has been detected by the Snips Platform. For instance, for a connected alarm clock, on may want to add a weather skill and a radio skill which react to `WeatherForecast` and `TurnOnRadio` intents, respectively. 17 | 18 | The hotword, ASR, and NLU modules are configured in the [Snips Console](https://console.snips.ai). This is a web interface that allows you to specify the language of the system, to define the types of queries that the system should understand, and to provide custom training examples that correspond exactly to the intended use. 19 | 20 | ## What is the Snips Assistant Manager? 21 | 22 | The Snips Assistant Manager is a set of command-line utilities for managing assistants built with Snips. With it, you can completely specify the behaviour of your assistant without a single line of code. 23 | 24 | All the information about an assistant is contained in a file, the Snipsfile. In it, you can specify: 25 | 26 | - The location of your assistant models 27 | - The skills you want to include, and accompanying parameters, such as an API key or a username 28 | - Optionally, how skills should react to intents 29 | - Various general configuration parameters, such as microphone type or Text-to-Speech service 30 | 31 | 32 | 33 | ## Installation 34 | 35 | ## Creating a Snipsfile 36 | 37 | First, let's create a simple **Snipsfile**, and put it in an empty folder on the Raspberry Pi: 38 | 39 | {% highlight yaml %} 40 | assistant_url: "https://github.com/snipsco/example-assistants/raw/master/weather-assistant.zip" 41 | locale: en_US 42 | default_location: Paris,fr 43 | tts: 44 | service: snips 45 | skills: 46 | - package_name: snipsfakeweather 47 | pip: snips-skill-fakeweather 48 | requires_tts: True 49 | {% endhighlight %} 50 | 51 | This Snipsfile points to a weather assistant, as created in the [Snips Console](https://console.snips.ai), using the prepackaged Weather Bundle. It uses the Snips on-device text-to-speech engine, and binds to a single skill, [snipsfakeweather](https://github.com/snipsco/snips-skill-fakeweather), which, given a date and a location, generates a fake spoken weather forecast. 52 | 53 | ## Installing dependencies 54 | 55 | From the command line, we place ourselves at the location of the Snipsfile, and run: 56 | 57 | ```sh 58 | $ snipsskills install 59 | ``` 60 | 61 | This will download the language models, create bindings between intents and skills, and optionally install boot scripts (for the assistant to be launched at start) as well as Bluetooth dependencies. 62 | 63 | After the installation has completed, we are ready to run the assistant! 64 | 65 | ## Running 66 | 67 | Simply run the following: 68 | 69 | ```sh 70 | $ snipsskills run 71 | ``` 72 | 73 | After a few seconds, you will hear a gentle wake up sound, indicating that the assistant is ready to take your voice commands. We can now start speaking: 74 | 75 | > Hey Snips 76 | 77 | > What is the weather going to be in Chicago this week-end? 78 | 79 | If all works well, you should get a reply speaking out a fake weather forecast! -------------------------------------------------------------------------------- /docs/_posts/2017-10-02-Installation.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Installation" 4 | date: 2017-10-02 12:53:25 +0200 5 | permalink: /installation/ 6 | --- 7 | 8 | Snips Skills runs on Raspbian and macOS. The latter platform is intended mainly for developing skills, but it does not run the Snips Platform yet. 9 | 10 | ## Raspberry Pi 11 | 12 | On Raspbian, Snips Skills can be installed in three ways: either with the custom Snips Raspbian image, which includes all the libraries necessary to run both the Snips Platform, and Snips Skills itself. It also comes preinstalled with a Bluetooth service, allowing you to control the Raspberry Pi direclty with your phone via the [iOS App]({{ site.baseurl }}/{% post_url 2017-10-02-iOS-App %}). 13 | 14 | ### Raspbian image 15 | 16 | Download the [Snips Raspbian Image]() and burn it to an SD card, for instance using [Etcher](https://etcher.io). Insert the SD card in your Raspberry Pi, and boot. 17 | 18 | In order to get your Raspberry Pi connected to your Wi-Fi network, you can use the companion [iOS App]({{ site.baseurl }}/{% post_url 2017-10-02-iOS-App %}). Alternatively, you can follow the [instructions](https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md) on the Raspberry Pi website. 19 | 20 | ### Debian package 21 | 22 | If you already have a working Raspberry Pi setup, you can install Snips Skills using `apt-get`. You will first need to update the Debian package repository: 23 | 24 | - Create a file `/etc/apt/sources.list.d/snips.list` (we use `snips.list` here, but any name will work) 25 | - Add the following line: `deb https://s3.amazonaws.com/snips-deb/ stable main` 26 | - Run `sudo apt-get update` to update the repository 27 | 28 | Snips Skills can now be installed: 29 | 30 | ```sh 31 | $ sudo apt-get install snipsskills 32 | ``` 33 | 34 | ### Python package 35 | 36 | Snips Skills also comes as a `pip` package. This however requires installing a few dependencies beforehand. Start by running: 37 | 38 | ```sh 39 | $ sudo apt-get update 40 | $ sudo apt-get install python-pip libsdl-mixer1.2 libusb-1.0 \ 41 | python-pyaudio libsdl1.2-dev cython cython3 libudev-dev \ 42 | python-dev libsdl-image1.2-dev libsdl-mixer1.2-dev \ 43 | libsdl-ttf2.0-dev libsmpeg-dev python-numpy libportmidi-dev \ 44 | libswscale-dev libavformat-dev libavcodec-dev \ 45 | portaudio19-dev nodejs build-essential -y 46 | ``` 47 | 48 | Next, we want to create a Python virtual environment to avoid conflicts with existing dependencies, and to be able to run Snips Skills without root privileges: 49 | 50 | ```sh 51 | $ sudo pip install --upgrade virtualenv 52 | $ virtualenv --python=/usr/bin/python2.7 snipsskills-env 53 | $ source snipsskills-env/bin/activate 54 | (snipsskills-env) $ pip install pip --upgrade 55 | ``` 56 | 57 | You may replace `snipsskills-env` with any name for your virtual environment. 58 | 59 | We are ready to install the `snipsskills` package: 60 | 61 | ```sh 62 | (snipsskills-env) $ pip install snipsskills 63 | ``` 64 | 65 | ## macOS 66 | 67 | On macOS, Snips Skills is also available as a `pip` package. To install, Portaudio, Pyaudio and SDL are needed: 68 | 69 | ```sh 70 | $ sudo easy_install pip 71 | $ brew install portaudio 72 | $ brew install sdl 73 | $ pip install --global-option='build_ext' \ 74 | --global-option='-I/usr/local/include' \ 75 | --global-option='-L/usr/local/lib' pyaudio 76 | ``` 77 | 78 | Next, like with Raspbian, we create a Python virtual environment in which Snips Skills will be run: 79 | 80 | ```sh 81 | $ sudo pip install --upgrade virtualenv 82 | $ virtualenv --python=/usr/bin/python2.7 snipsskills-env 83 | $ source snipsskills-env/bin/activate 84 | (snipsskills-env) $ pip install pip --upgrade 85 | ``` 86 | 87 | Snips Skills can now be installed. 88 | 89 | ```sh 90 | (snipsskills-env) $ pip install snipsskills 91 | ``` 92 | -------------------------------------------------------------------------------- /docs/_posts/2017-10-02-Microphone-Setup.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Microphone Setup" 4 | date: 2017-10-02 12:53:25 +0200 5 | permalink: /microphones/ 6 | --- 7 | 8 | We have streamlined the process of setting up a microphone using Snips Assistant Manager, so it should just be a matter of specifying the microphone model in the Snipsfile, and Sam will update drivers and setup acoundrc configurations. If you want recommendation for which microphones to use, check out our [Microphone Array Benchmark](medium.com/snips-ai/benchmarking-microphone-arrays-respeaker-conexant-microsemi-acuedge-matrix-creator-minidsp-950de8876fda). 9 | 10 | On this page, we compile the setup instructions for the most popular microphones for Raspberry Pi. 11 | 12 | ## ReSpeaker 13 | 14 | Simply add the following to your Snipsfile: 15 | 16 | ```yaml 17 | microphone: 18 | identifier: respeaker 19 | params: 20 | vendor_id: "2886" 21 | product_id: "0007" 22 | ``` 23 | 24 | You need to change the `vendor_id` and `product_id` to match those of your setup. You can obtain these by running the `lsusb` command. 25 | 26 | ```sh 27 | $ lsusb 28 | Bus 001 Device 004: ID 2886:0007 ReSpeaker 29 | ... 30 | ``` 31 | 32 | ## Jabra 33 | 34 | Add the following to your Snipsfile: 35 | 36 | ```yaml 37 | microphone: 38 | identifier: jabra 39 | ``` 40 | 41 | ## Other microphones 42 | 43 | Most other microphones do not require a custom setup. You don't need to add anything to your Snipsfile. However, if you have trouble setting one up, please let us know and we will try to include it in our toolkit. -------------------------------------------------------------------------------- /docs/_posts/2017-10-02-Troubleshooting-iOS.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: plain 3 | title: "Troubleshooting" 4 | date: 2017-10-02 12:53:25 +0200 5 | permalink: /troubleshooting-ios/ 6 | --- 7 | 8 | ## Troubleshooting 9 | -------------------------------------------------------------------------------- /docs/_posts/2017-10-02-Troubleshooting.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Troubleshooting" 4 | date: 2017-10-02 12:53:25 +0200 5 | permalink: /troubleshooting/ 6 | --- 7 | 8 | ## Troubleshooting -------------------------------------------------------------------------------- /docs/_posts/2017-10-02-iOS-App.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Snips Control" 4 | date: 2017-10-02 12:53:25 +0200 5 | permalink: /ios/ 6 | --- 7 | 8 |
9 | 10 |
11 | 12 | With the Snips Control app for iOS, you can configure and control your Snips device over Bluetooth. 13 | 14 | In the app, you can: 15 | 16 | * Load an assistant remotely, either from the [Snips Console](https://console.snips.ai), a public URL, or a locally available file 17 | * Choose a microphone configuration 18 | * Update the Wi-Fi credentials 19 | * Install skills from the public [Snips Skills registry](https://github.com/snipsco/skills-registry) 20 | * Inspect the intents being detected by the Snips engine, via a chat-like dashboard 21 | 22 | The app is currently available for iOS. The Android version is in the works. 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/_sass/buttons.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | @import "mixins"; 3 | 4 | .button-rounded { 5 | @include border-radius(25px); 6 | font-weight:500; 7 | font-size:18; 8 | height:50px; 9 | box-sizing: border-box; 10 | text-align:center; 11 | padding: 15px 30px 15px 30px; 12 | } 13 | 14 | .cta-button-green { 15 | @extend .button-rounded; 16 | font-weight:500; 17 | background-color:$green; 18 | color:white; 19 | text-decoration:none; 20 | font-family: "apercu"; 21 | } 22 | 23 | .field-grey { 24 | @extend .button-rounded; 25 | @include border-radius(5px); 26 | padding: 15px; 27 | font-weight:500; 28 | background-color:$darkergrey; 29 | color:white; 30 | text-decoration:none; 31 | font-family: "apercu"; 32 | } 33 | -------------------------------------------------------------------------------- /docs/_sass/colors.scss: -------------------------------------------------------------------------------- 1 | $blue: #001EFF; 2 | $darkblue: #0015B0; 3 | $green: #00CD6A; 4 | $meddarkgreen: #00B960; 5 | $darkgreen: #004D28; 6 | $lightgrey: #F1F1F1; 7 | $darkgrey: #CBCBCB; 8 | $darkergrey: #555555; 9 | 10 | $text-color: #2e2e2e; 11 | $text-color-light: #888888; 12 | -------------------------------------------------------------------------------- /docs/_sass/highlight.scss: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight .c { color: #999988; font-style: italic } /* Comment */ 3 | .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ 4 | .highlight .k { color: #000000; font-weight: bold } /* Keyword */ 5 | .highlight .o { color: #000000; font-weight: bold } /* Operator */ 6 | .highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #999999; font-weight: bold; font-style: italic } /* Comment.Preproc */ 8 | .highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ 9 | .highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ 10 | .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ 11 | .highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ 12 | .highlight .gr { color: #aa0000 } /* Generic.Error */ 13 | .highlight .gh { color: #999999 } /* Generic.Heading */ 14 | .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ 15 | .highlight .go { color: #888888 } /* Generic.Output */ 16 | .highlight .gp { color: #555555 } /* Generic.Prompt */ 17 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 18 | .highlight .gu { color: #aaaaaa } /* Generic.Subheading */ 19 | .highlight .gt { color: #aa0000 } /* Generic.Traceback */ 20 | .highlight .kc { color: #000000; font-weight: bold } /* Keyword.Constant */ 21 | .highlight .kd { color: #000000; font-weight: bold } /* Keyword.Declaration */ 22 | .highlight .kn { color: #000000; font-weight: bold } /* Keyword.Namespace */ 23 | .highlight .kp { color: #000000; font-weight: bold } /* Keyword.Pseudo */ 24 | .highlight .kr { color: #000000; font-weight: bold } /* Keyword.Reserved */ 25 | .highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ 26 | .highlight .m { color: #009999 } /* Literal.Number */ 27 | .highlight .s { color: #d01040 } /* Literal.String */ 28 | .highlight .na { color: #008080 } /* Name.Attribute */ 29 | .highlight .nb { color: #0086B3 } /* Name.Builtin */ 30 | .highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ 31 | .highlight .no { color: #008080 } /* Name.Constant */ 32 | .highlight .nd { color: #3c5d5d; font-weight: bold } /* Name.Decorator */ 33 | .highlight .ni { color: #800080 } /* Name.Entity */ 34 | .highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ 35 | .highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ 36 | .highlight .nl { color: #990000; font-weight: bold } /* Name.Label */ 37 | .highlight .nn { color: #555555 } /* Name.Namespace */ 38 | .highlight .nt { color: #000080 } /* Name.Tag */ 39 | .highlight .nv { color: #008080 } /* Name.Variable */ 40 | .highlight .ow { color: #000000; font-weight: bold } /* Operator.Word */ 41 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .highlight .mf { color: #009999 } /* Literal.Number.Float */ 43 | .highlight .mh { color: #009999 } /* Literal.Number.Hex */ 44 | .highlight .mi { color: #009999 } /* Literal.Number.Integer */ 45 | .highlight .mo { color: #009999 } /* Literal.Number.Oct */ 46 | .highlight .sb { color: #d01040 } /* Literal.String.Backtick */ 47 | .highlight .sc { color: #d01040 } /* Literal.String.Char */ 48 | .highlight .sd { color: #d01040 } /* Literal.String.Doc */ 49 | .highlight .s2 { color: #d01040 } /* Literal.String.Double */ 50 | .highlight .se { color: #d01040 } /* Literal.String.Escape */ 51 | .highlight .sh { color: #d01040 } /* Literal.String.Heredoc */ 52 | .highlight .si { color: #d01040 } /* Literal.String.Interpol */ 53 | .highlight .sx { color: #d01040 } /* Literal.String.Other */ 54 | .highlight .sr { color: #009926 } /* Literal.String.Regex */ 55 | .highlight .s1 { color: #d01040 } /* Literal.String.Single */ 56 | .highlight .ss { color: #990073 } /* Literal.String.Symbol */ 57 | .highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ 58 | .highlight .vc { color: #008080 } /* Name.Variable.Class */ 59 | .highlight .vg { color: #008080 } /* Name.Variable.Global */ 60 | .highlight .vi { color: #008080 } /* Name.Variable.Instance */ 61 | .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/_sass/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin border-radius($radius) { 2 | -webkit-border-radius: $radius; 3 | -moz-border-radius: $radius; 4 | -ms-border-radius: $radius; 5 | border-radius: $radius; 6 | } 7 | 8 | @mixin box-sizing($box-sizing) { 9 | -webkit-box-sizing: $box-sizing; 10 | -moz-box-sizing: $box-sizing; 11 | box-sizing: $box-sizing; 12 | } 13 | -------------------------------------------------------------------------------- /docs/_sass/typography.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "apercu"; 3 | src: url($baseurl + '/fonts/apercu_regular.ttf'); 4 | font-style: normal; 5 | } 6 | 7 | @font-face { 8 | font-family: "apercu"; 9 | src: url($baseurl + '/fonts/apercu_medium.ttf'); 10 | font-width: 500; 11 | } 12 | 13 | @font-face { 14 | font-family: "apercu_mono"; 15 | src: url($baseurl + '/fonts/apercu_mono.ttf'); 16 | font-style: normal; 17 | } 18 | 19 | $font-default: -apple-system,BlinkMacSystemFont,Avenir,"Avenir Next","Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue"; 20 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: About 4 | permalink: /about/ 5 | --- 6 | 7 | This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](https://jekyllrb.com/) 8 | 9 | You can find the source code for Minima at GitHub: 10 | [jekyll][jekyll-organization] / 11 | [minima](https://github.com/jekyll/minima) 12 | 13 | You can find the source code for Jekyll at GitHub: 14 | [jekyll][jekyll-organization] / 15 | [jekyll](https://github.com/jekyll/jekyll) 16 | 17 | 18 | [jekyll-organization]: https://github.com/jekyll 19 | -------------------------------------------------------------------------------- /docs/css/font-awesome/HELP-US-OUT.txt: -------------------------------------------------------------------------------- 1 | I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project, 2 | Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome, 3 | comprehensive icon sets or copy and paste your own. 4 | 5 | Please. Check it out. 6 | 7 | -Dave Gandy 8 | -------------------------------------------------------------------------------- /docs/css/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/css/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /docs/css/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/css/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/css/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/css/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/css/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/css/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/css/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/css/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /docs/css/font-awesome/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/css/font-awesome/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /docs/css/font-awesome/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /docs/css/font-awesome/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /docs/css/font-awesome/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | @import "screen-reader.less"; 19 | -------------------------------------------------------------------------------- /docs/css/font-awesome/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /docs/css/font-awesome/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/css/font-awesome/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | .fa-icon-rotate(@degrees, @rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; 16 | -webkit-transform: rotate(@degrees); 17 | -ms-transform: rotate(@degrees); 18 | transform: rotate(@degrees); 19 | } 20 | 21 | .fa-icon-flip(@horiz, @vert, @rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; 23 | -webkit-transform: scale(@horiz, @vert); 24 | -ms-transform: scale(@horiz, @vert); 25 | transform: scale(@horiz, @vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | .sr-only() { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | .sr-only-focusable() { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /docs/css/font-awesome/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /docs/css/font-awesome/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /docs/css/font-awesome/less/screen-reader.less: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { .sr-only(); } 5 | .sr-only-focusable { .sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /docs/css/font-awesome/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /docs/css/font-awesome/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/css/font-awesome/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /docs/css/font-awesome/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /docs/css/font-awesome/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /docs/css/font-awesome/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /docs/css/font-awesome/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/css/font-awesome/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | @mixin sr-only { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | @mixin sr-only-focusable { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /docs/css/font-awesome/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /docs/css/font-awesome/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /docs/css/font-awesome/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /docs/css/font-awesome/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /docs/css/font-awesome/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /docs/css/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | # this ensures Jekyll reads the file to be transformed into CSS later 3 | # only Main files contain this front matter, not partials. 4 | --- 5 | 6 | $baseurl: {{ site.baseurl }}; 7 | 8 | @import "typography"; 9 | @import "colors"; 10 | @import "buttons"; 11 | @import "mixins"; 12 | @import "highlight"; 13 | 14 | body { 15 | margin:0; 16 | padding:0; 17 | font-family:$font-default; 18 | -webkit-font-smoothing: antialiased; 19 | } 20 | 21 | .content { 22 | max-width: 940px; 23 | margin: 0 auto; 24 | p, li { 25 | font-size: 16px; 26 | font-weight:400; 27 | line-height:1.6; 28 | color:$text-color; 29 | } 30 | } 31 | 32 | .highlight-box { 33 | @include border-radius(15px); 34 | padding: 10px 30px; 35 | border-style: dotted; 36 | border-color: $darkgrey; 37 | border-width: 2px; 38 | background-color:$lightgrey; 39 | } 40 | 41 | a { 42 | text-decoration:none; 43 | color:$blue; 44 | } 45 | 46 | h1,h2,h3,h4 { 47 | font-family: "apercu"; 48 | font-weight:500; 49 | -webkit-font-smoothing: antialiased; 50 | } 51 | 52 | h1 { 53 | font-size:44px; 54 | } 55 | 56 | h2 { 57 | font-size:24px; 58 | } 59 | 60 | h5 { 61 | font-family: "apercu-mono"; 62 | code { 63 | font-family: "apercu-mono"; 64 | font-size:22px; 65 | white-space: pre; 66 | background-color:$blue; 67 | color:#fff; 68 | padding: 5px 10px; 69 | } 70 | } 71 | 72 | figure { 73 | margin:0; 74 | } 75 | 76 | pre { 77 | @include box-sizing(border-box); 78 | @include border-radius(5px); 79 | background-color:$lightgrey; 80 | padding: 10px 15px; 81 | overflow:scroll; 82 | code { 83 | padding: 0 !important; 84 | line-height:1.5; 85 | } 86 | } 87 | 88 | code { 89 | font-family: "apercu-mono"; 90 | font-size:14px; 91 | background-color:$lightgrey; 92 | padding: 2px 5px; 93 | @include border-radius(5px); 94 | } 95 | 96 | code.nobackground { 97 | background-color:transparent !important; 98 | } 99 | 100 | ::-webkit-scrollbar { 101 | display: none; 102 | } 103 | 104 | #menu { 105 | background-color:$darkblue; 106 | height:100px; 107 | .title { 108 | -webkit-font-smoothing: antialiased; 109 | font-family: "apercu"; 110 | font-size: 24px; 111 | font-weight: 500; 112 | color:#fff; 113 | padding-top:34px; 114 | display:block; 115 | float:left; 116 | a { 117 | text-decoration:none; 118 | color:#fff; 119 | } 120 | } 121 | a.github { 122 | // display:block; 123 | } 124 | .links { 125 | margin-top:35px; 126 | float:right; 127 | ul { 128 | list-style-type: none; 129 | margin:0; 130 | padding:0; 131 | li { 132 | display: inline; 133 | list-style-type: none; 134 | padding-left: 20px; 135 | a { 136 | font-family:$font-default; 137 | font-weight:500; 138 | color:#fff; 139 | text-decoration:none; 140 | } 141 | a.selected { 142 | text-decoration:underline; 143 | } 144 | a:hover { 145 | text-decoration:underline; 146 | } 147 | a.github:hover { 148 | text-decoration:none; 149 | background-color:$meddarkgreen; 150 | } 151 | } 152 | } 153 | } 154 | 155 | } 156 | 157 | #footer { 158 | background-color:$blue; 159 | height:100px; 160 | margin-top:100px; 161 | img { 162 | float: left; 163 | margin-top:33px; 164 | } 165 | .links { 166 | float:right; 167 | margin-top:40px; 168 | span { 169 | color:#fff; 170 | text-decoration:none; 171 | } 172 | a { 173 | color:#fff; 174 | text-decoration:none; 175 | font-weight:500; 176 | padding-left:20px; 177 | } 178 | } 179 | .title { 180 | -webkit-font-smoothing: antialiased; 181 | font-family: "apercu"; 182 | font-size: 24px; 183 | font-weight: 500; 184 | color:#fff; 185 | padding-top:34px; 186 | display:block; 187 | float:left; 188 | a { 189 | text-decoration:none; 190 | color:#fff; 191 | } 192 | } 193 | a.github { 194 | margin-top:25px; 195 | float:right; 196 | display:block; 197 | } 198 | } 199 | 200 | #hero { 201 | background-color:$blue; 202 | height:300px; 203 | .header { 204 | font-family: "apercu"; 205 | font-size: 44px; 206 | color: #fff; 207 | margin:0; 208 | padding-top:70px; 209 | max-width:60%; 210 | float:left; 211 | } 212 | img { 213 | float:right; 214 | margin-top:135px; 215 | } 216 | } 217 | 218 | .main { 219 | .command { 220 | @extend .button-rounded; 221 | margin:0 auto; 222 | background-color: $green; 223 | display:block; 224 | max-width:380px; 225 | font-family: "apercu-mono"; 226 | color:#fff; 227 | position:relative; 228 | margin-top:10px; 229 | -webkit-font-smoothing: antialiased; 230 | img { 231 | position:absolute; 232 | left:20px; 233 | top:10px; 234 | } 235 | span { 236 | width:100%; 237 | text-align:center; 238 | padding-top:-5px; 239 | span { 240 | color:$darkgreen; 241 | } 242 | } 243 | } 244 | 245 | .linux-command { 246 | margin-top:60px; 247 | } 248 | 249 | .command span::selection { 250 | background: $darkgreen; 251 | } 252 | .command span::-moz-selection { 253 | background: $darkgreen; 254 | } 255 | 256 | .mini-info { 257 | text-align:center; 258 | font-size:12px; 259 | font-family:$font-default; 260 | color:$text-color-light; 261 | width:75%; 262 | margin:0 auto; 263 | margin-top:30px; 264 | code { 265 | color:$text-color; 266 | background-color:none !important; 267 | font-size:12px; 268 | } 269 | } 270 | 271 | article { 272 | @include box-sizing(border-box); 273 | max-width:640px; 274 | padding-right:20px; 275 | } 276 | 277 | nav { 278 | @include box-sizing(border-box); 279 | @extend .highlight-box; 280 | width:240px; 281 | float:right; 282 | padding-left:10px; 283 | @include border-radius(10px); 284 | ul { 285 | list-style-type: none; 286 | margin:0; 287 | padding:0; 288 | li { 289 | padding:10px 5px; 290 | a { 291 | text-decoration: none; 292 | font-family: "apercu"; 293 | color:$text-color; 294 | font-weight: 500; 295 | } 296 | a.selected { 297 | color:$blue; 298 | // text-decoration:underline; 299 | text-decoration:none; 300 | font-weight:500; 301 | } 302 | } 303 | } 304 | } 305 | } 306 | 307 | .info .content { 308 | @extend .highlight-box; 309 | @include box-sizing(border-box); 310 | margin-top:60px; 311 | p { 312 | font-size: 18px; 313 | line-height: 28px; 314 | color: $text-color-light; 315 | a { 316 | text-decoration: underline; 317 | color: $blue; 318 | font-weight: 500; 319 | } 320 | } 321 | } 322 | 323 | .skill { 324 | height:120px; 325 | border-bottom:1px solid $lightgrey; 326 | @include box-sizing(border-box); 327 | img { 328 | float:left; 329 | margin-top:10px; 330 | margin-left:15px; 331 | margin-right:25px; 332 | } 333 | h1 { 334 | color:#000; 335 | font-size:22px; 336 | font-weight:500; 337 | padding-top:10px; 338 | margin-bottom:0; 339 | line-height:29px; 340 | } 341 | p { 342 | margin-bottom:0; 343 | margin-top:2px; 344 | padding:0; 345 | font-size:16px; 346 | } 347 | .links { 348 | margin-top:5px; 349 | i { 350 | margin-right:2px; 351 | } 352 | a { 353 | font-size:14px; 354 | display:inline-block; 355 | color:$blue; 356 | padding-right:20px; 357 | } 358 | } 359 | } -------------------------------------------------------------------------------- /docs/fonts/apercu_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/fonts/apercu_bold.ttf -------------------------------------------------------------------------------- /docs/fonts/apercu_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/fonts/apercu_medium.ttf -------------------------------------------------------------------------------- /docs/fonts/apercu_mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/fonts/apercu_mono.ttf -------------------------------------------------------------------------------- /docs/fonts/apercu_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/fonts/apercu_regular.ttf -------------------------------------------------------------------------------- /docs/images/AppStoreBadge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/AppStoreBadge.png -------------------------------------------------------------------------------- /docs/images/AppStoreBadge@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/AppStoreBadge@2x.png -------------------------------------------------------------------------------- /docs/images/Cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/Cover.png -------------------------------------------------------------------------------- /docs/images/Cover@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/Cover@2x.png -------------------------------------------------------------------------------- /docs/images/IconApple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/IconApple.png -------------------------------------------------------------------------------- /docs/images/IconApple@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/IconApple@2x.png -------------------------------------------------------------------------------- /docs/images/IconLinux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/IconLinux.png -------------------------------------------------------------------------------- /docs/images/IconLinux@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/IconLinux@2x.png -------------------------------------------------------------------------------- /docs/images/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/Logo.png -------------------------------------------------------------------------------- /docs/images/Logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/Logo@2x.png -------------------------------------------------------------------------------- /docs/images/Platform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/Platform.png -------------------------------------------------------------------------------- /docs/images/Platform@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/Platform@2x.png -------------------------------------------------------------------------------- /docs/images/Snips Spotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/Snips Spotify.png -------------------------------------------------------------------------------- /docs/images/Snips Spotify@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/Snips Spotify@2x.png -------------------------------------------------------------------------------- /docs/images/iPhone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/iPhone.png -------------------------------------------------------------------------------- /docs/images/iPhone@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/iPhone@2x.png -------------------------------------------------------------------------------- /docs/images/skills/Coffee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Coffee.png -------------------------------------------------------------------------------- /docs/images/skills/Coffee@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Coffee@2x.png -------------------------------------------------------------------------------- /docs/images/skills/Hue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Hue.png -------------------------------------------------------------------------------- /docs/images/skills/Hue@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Hue@2x.png -------------------------------------------------------------------------------- /docs/images/skills/Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Light.png -------------------------------------------------------------------------------- /docs/images/skills/Light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Light@2x.png -------------------------------------------------------------------------------- /docs/images/skills/Music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Music.png -------------------------------------------------------------------------------- /docs/images/skills/Music@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Music@2x.png -------------------------------------------------------------------------------- /docs/images/skills/OWM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/OWM.png -------------------------------------------------------------------------------- /docs/images/skills/OWM@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/OWM@2x.png -------------------------------------------------------------------------------- /docs/images/skills/Smarter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Smarter.png -------------------------------------------------------------------------------- /docs/images/skills/Smarter@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Smarter@2x.png -------------------------------------------------------------------------------- /docs/images/skills/Sonos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Sonos.png -------------------------------------------------------------------------------- /docs/images/skills/Sonos@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Sonos@2x.png -------------------------------------------------------------------------------- /docs/images/skills/Weather.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Weather.png -------------------------------------------------------------------------------- /docs/images/skills/Weather@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/docs/images/skills/Weather@2x.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # You don't need to edit this file, it's empty on purpose. 3 | # Edit theme's home layout instead if you wanna make some changes 4 | # See: https://jekyllrb.com/docs/themes/#overriding-theme-defaults 5 | layout: home 6 | title: Snips Skills Manager 7 | --- 8 | 9 | # Snips Skills Manager -------------------------------------------------------------------------------- /docs/spotify-login.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: spotify-login 3 | title: Spotify Web Login 4 | --- 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Packaging settings.""" 2 | 3 | from setuptools import Command, find_packages, setup 4 | from snipsmanager import __version__ 5 | from codecs import open 6 | from os.path import abspath, dirname, join 7 | from subprocess import call 8 | 9 | this_dir = abspath(dirname(__file__)) 10 | with open(join(this_dir, 'README.md'), encoding='utf-8') as file: 11 | long_description = file.read() 12 | 13 | 14 | class RunTests(Command): 15 | """Run all tests.""" 16 | description = 'run tests' 17 | user_options = [] 18 | 19 | def initialize_options(self): 20 | pass 21 | 22 | def finalize_options(self): 23 | pass 24 | 25 | def run(self): 26 | """Run all tests!""" 27 | errno = call(['py.test', '--cov=snipsmanager', '--cov-report=term-missing']) 28 | raise SystemExit(errno) 29 | 30 | 31 | setup( 32 | name = 'snipsmanager', 33 | version = __version__, 34 | description = 'Snips Manager', 35 | long_description = long_description, 36 | url = 'https://github.com/snipsco/snipsmanager', 37 | author = 'Snips', 38 | author_email = 'labs@snips.ai', 39 | license='MIT', 40 | keywords = ['cli', 'snips'], 41 | packages = find_packages(exclude=['docs', 'tests*']), 42 | install_requires = [ 43 | 'docopt', 44 | 'python-dateutil', 45 | 'Jinja2', 46 | 'pyyaml', 47 | 'pip', 48 | 'snipsmanagercore' 49 | ], 50 | extras_require = { 51 | 'test': ['coverage', 'pytest', 'pytest-cov'], 52 | }, 53 | entry_points = { 54 | 'console_scripts': [ 55 | 'snipsmanager=snipsmanager.cli:main', 56 | ], 57 | }, 58 | include_package_data=True, 59 | cmdclass = { 'test': RunTests } 60 | ) 61 | -------------------------------------------------------------------------------- /snipsmanager/__init__.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ snipsmanager module """ 3 | __version__ = '0.2.0.0' 4 | 5 | import os 6 | import logging 7 | import subprocess 8 | import sys 9 | 10 | def which(command, default_value): 11 | try: 12 | return subprocess.check_output(["which", command]).strip() 13 | except subprocess.CalledProcessError: 14 | return default_value 15 | 16 | HOME_DIR = os.path.expanduser('~') 17 | if 'arm' in " ".join(os.uname()): 18 | HOME_DIR = "/home/pi" 19 | 20 | PACKAGE_NAME = "snipsmanager" 21 | SNIPS_CACHE_DIR_NAME = ".snips" 22 | SNIPS_CACHE_DIR = os.path.join(HOME_DIR, SNIPS_CACHE_DIR_NAME) 23 | NODE_MODULES_PARENT_DIR = SNIPS_CACHE_DIR 24 | NODE_MODULES_DIR = os.path.join(NODE_MODULES_PARENT_DIR, "node_modules") 25 | DEFAULT_SNIPSFILE_PATH = os.path.join(os.getcwd(), "Snipsfile") 26 | SNIPS_CACHE_INTENTS_DIR = os.path.join(SNIPS_CACHE_DIR, "intents") 27 | SNIPS_CACHE_INTENT_REGISTRY_FILE = os.path.join(SNIPS_CACHE_INTENTS_DIR, "intent_registry.py") 28 | ASOUNDCONF_DEST_PATH = "/etc/asound.conf" 29 | 30 | SHELL_COMMAND = which("bash", "/bin/bash") 31 | 32 | this_dir, this_filename = os.path.split(__file__) 33 | __DEB_VENV = "/opt/venvs/{}".format(PACKAGE_NAME) 34 | if this_dir.startswith(__DEB_VENV): 35 | # We need to force this in Deb version 36 | VENV_PATH = __DEB_VENV 37 | PIP_BINARY = os.path.join(VENV_PATH, "bin/pip") 38 | elif 'VIRTUAL_ENV' in os.environ: 39 | VENV_PATH = os.environ['VIRTUAL_ENV'] 40 | PIP_BINARY = os.path.join(VENV_PATH, "bin/pip") 41 | else: 42 | VENV_PATH = None 43 | PIP_BINARY = which("pip", "/usr/local/bin/pip") 44 | 45 | def prepare_cache(): 46 | if not os.path.exists(HOME_DIR): 47 | os.makedirs(HOME_DIR) 48 | if not os.path.exists(SNIPS_CACHE_DIR): 49 | os.makedirs(SNIPS_CACHE_DIR) 50 | 51 | prepare_cache() 52 | 53 | logger = logging.getLogger(__name__) 54 | handler = logging.StreamHandler() 55 | log_format = '\033[2m%(asctime)s\033[0m [%(levelname)s] %(message)s' 56 | date_format = '%Y-%m-%d %H:%M:%S' 57 | formatter = logging.Formatter(log_format, date_format) 58 | handler.setFormatter(formatter) 59 | logger.addHandler(handler) 60 | logger.setLevel(logging.INFO) 61 | 62 | RESET_SEQ = u'\033[0m' 63 | GREEN_COLOR = u'\033[32m' 64 | BLUE_COLOR = u'\033[34m' 65 | -------------------------------------------------------------------------------- /snipsmanager/cli.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ snipsmanager 3 | 4 | Usage: 5 | snipsmanager install [--snipsfile= --skip-bluetooth --skip-systemd --force-download --silent --debug] [--email= --password=] 6 | snipsmanager install bluetooth [--force-download] 7 | snipsmanager install skill [--force-download --debug] 8 | snipsmanager install skills [--snipsfile= --silent] 9 | snipsmanager install addon [--silent --non-interactive] [PARAMS ...] 10 | snipsmanager fetch assistant [--snipsfile=] [--id= --url= --file=] [--email= --password=] [--force-download] 11 | snipsmanager load assistant [--file= --platform-only] 12 | snipsmanager setup microphone [--snipsfile=] [ [--skip-asoundconf] [PARAMS ...]] 13 | snipsmanager setup speaker [--snipsfile=] [ [--skip-asoundconf] [PARAMS ...]] 14 | snipsmanager setup systemd bluetooth [--mqtt-host= --mqtt-port=] 15 | snipsmanager setup systemd snips 16 | snipsmanager setup systemd skills [--snipsfile=] 17 | snipsmanager run [--snipsfile=] [--mqtt-host= --mqtt-port= --tts-service= --locale=] [--debug] 18 | snipsmanager login [--email= --password=] 19 | snipsmanager logout 20 | snipsmanager -h | --help 21 | snipsmanager --version 22 | 23 | Options: 24 | -h --help Show this screen. 25 | --version Show version. 26 | --snipsfile= Path to the Snipsfile. 27 | --skill= 28 | 29 | Examples: 30 | snipsmanager install 31 | 32 | Help: 33 | For help using this tool, please open an issue on the Github repository: 34 | https://github.com/snipsco/snipsmanager 35 | """ 36 | 37 | import os 38 | import sys 39 | 40 | from docopt import docopt 41 | from snipsmanagercore import pretty_printer as pp 42 | 43 | from . import logger 44 | from . import __version__ as VERSION 45 | 46 | def matches_options(options, option_string): 47 | values = option_string.split("/") 48 | for value in values: 49 | if options[value] != True: 50 | return False 51 | return True 52 | 53 | def main(): 54 | """ Main entry point. """ 55 | options = docopt(__doc__, version=VERSION) 56 | 57 | try: 58 | if options['setup'] == True and options['microphone'] == True: 59 | from snipsmanager.commands.setup.microphone import MicrophoneInstaller 60 | MicrophoneInstaller(options).run() 61 | elif options['setup'] == True and options['speaker'] == True: 62 | from snipsmanager.commands.setup.speaker import SpeakerInstaller 63 | SpeakerInstaller(options).run() 64 | elif options['setup'] == True and options['systemd'] == True and options['bluetooth'] == True: 65 | from snipsmanager.commands.setup.systemd.bluetooth import SystemdBluetooth 66 | SystemdBluetooth(options).run() 67 | elif options['setup'] == True and options['systemd'] == True and options['skills'] == True: 68 | from snipsmanager.commands.setup.systemd.snipsmanager import Systemdsnipsmanager 69 | Systemdsnipsmanager(options).run() 70 | elif options['login'] == True: 71 | from snipsmanager.commands.session.login import Login 72 | Login(options).run() 73 | elif options['logout'] == True: 74 | from snipsmanager.commands.session.logout import Logout 75 | Logout(options).run() 76 | elif options['fetch'] == True and options['assistant'] == True: 77 | from snipsmanager.commands.assistant.fetch import AssistantFetcher 78 | AssistantFetcher(options).run() 79 | elif options['load'] == True and options['assistant'] == True: 80 | from snipsmanager.commands.assistant.load import AssistantLoader 81 | AssistantLoader(options).run() 82 | elif options['install'] == True and options['bluetooth'] == True: 83 | from snipsmanager.commands.install.bluetooth import BluetoothInstaller 84 | BluetoothInstaller(options).run() 85 | elif options['install'] == True and options['skill'] == True: 86 | from snipsmanager.commands.install.skill import SkillInstaller 87 | SkillInstaller(options).run() 88 | elif options['install'] == True and options['addon'] == True: 89 | from snipsmanager.commands.install.addon import AddonInstaller 90 | AddonInstaller(options).run() 91 | elif options['install'] == True and options['skills'] == True: 92 | from snipsmanager.commands.install.skills import SkillsInstaller 93 | SkillsInstaller(options).run() 94 | elif options['install'] == True: 95 | from snipsmanager.commands.install.install import GlobalInstaller 96 | GlobalInstaller(options).run() 97 | except KeyboardInterrupt: 98 | try: 99 | print("\n") 100 | pp.perror("Snips Manager installer interrupted") 101 | sys.exit(0) 102 | except SystemExit: 103 | os._exit(0) 104 | 105 | try: 106 | if options['run'] == True: 107 | from snipsmanager.commands.run import Runner 108 | Runner(options).run() 109 | except KeyboardInterrupt: 110 | try: 111 | print("\n") 112 | logger.error("Snips Manager server stopped") 113 | sys.exit(0) 114 | except SystemExit: 115 | os._exit(0) 116 | 117 | # elif options['scaffold'] == True: 118 | # from snipsmanager.commands.scaffold import Scaffold 119 | # Scaffold().run() 120 | 121 | if __name__ == '__main__': 122 | main() -------------------------------------------------------------------------------- /snipsmanager/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/snipsmanager/commands/__init__.py -------------------------------------------------------------------------------- /snipsmanager/commands/assistant/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/snipsmanager/commands/assistant/__init__.py -------------------------------------------------------------------------------- /snipsmanager/commands/assistant/fetch.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | 3 | import hashlib 4 | import os 5 | import shutil 6 | import time 7 | 8 | from ..base import Base 9 | from ..session.login import Login 10 | from ..session.logout import Logout 11 | from ...utils.http_helpers import fetch_url 12 | from ...utils.os_helpers import write_binary_file, file_exists 13 | from ...utils.cache import Cache 14 | from ...utils.snipsfile import Snipsfile 15 | 16 | from ... import SNIPS_CACHE_DIR, DEFAULT_SNIPSFILE_PATH 17 | 18 | from snipsmanagercore import pretty_printer as pp 19 | 20 | class AssistantFetcherException(Exception): 21 | pass 22 | 23 | # pylint: disable=too-few-public-methods 24 | class AssistantFetcher(Base): 25 | 26 | SNIPS_TEMP_ASSISTANT_FILENAME = "assistant.zip" 27 | SNIPS_TEMP_ASSISTANT_PATH = os.path.join(SNIPS_CACHE_DIR, SNIPS_TEMP_ASSISTANT_FILENAME) 28 | CONSOLE_ASSISTANT_URL = "https://external-gateway.snips.ai/v1/assistant/{}/download" 29 | # CONSOLE_ASSISTANT_URL = "https://console.snips.ai/api/assistants/{}/download" 30 | 31 | 32 | def run(self): 33 | """ Command runner. 34 | 35 | Docopt command: 36 | 37 | snipsmanager fetch assistant [--id= --url= --file=] 38 | """ 39 | force_download = self.options['--force-download'] 40 | try: 41 | aid = self.options['--id'] 42 | url = self.options['--url'] 43 | file = self.options['--file'] 44 | email = self.options['--email'] 45 | password = self.options['--password'] 46 | if aid is not None or url is not None or file is not None: 47 | AssistantFetcher.fetch_from_params(aid=aid, url=url, file=file, email=email, password=password, force_download=force_download) 48 | else: 49 | AssistantFetcher.fetch(self.options['--snipsfile'], email=email, password=password, force_download=force_download) 50 | except Exception as e: 51 | pp.perror(str(e)) 52 | 53 | 54 | @staticmethod 55 | def fetch(snipsfile_path=None, email=None, password=None, force_download=False): 56 | if snipsfile_path is None: 57 | snipsfile_path = DEFAULT_SNIPSFILE_PATH 58 | if snipsfile_path is not None and not file_exists(snipsfile_path): 59 | raise AssistantFetcherException("Error fetching assistant: Snipsfile not found") 60 | snipsfile = Snipsfile(snipsfile_path) 61 | AssistantFetcher.fetch_from_snipsfile(snipsfile, email=email, password=password, force_download=force_download) 62 | 63 | 64 | @staticmethod 65 | def fetch_from_snipsfile(snipsfile, email=None, password=None, force_download=False): 66 | if snipsfile is None: 67 | raise AssistantFetcherException("Error fetching assistant: Snipsfile not found") 68 | AssistantFetcher.fetch_from_params(aid=snipsfile.assistant_id, url=snipsfile.assistant_url, file=snipsfile.assistant_file, email=email, password=password, force_download=force_download) 69 | 70 | 71 | @staticmethod 72 | def fetch_from_params(aid=None, url=None, file=None, email=None, password=None, force_download=False): 73 | pp.pcommand("Fetching assistant") 74 | 75 | if aid is None and url is None and file is None: 76 | raise AssistantFetcherException("Error fetching assistant. Please provide an assistant ID, a public URL, or a filename") 77 | 78 | if url: 79 | AssistantFetcher.download_public_assistant(url, force_download=force_download) 80 | elif aid: 81 | AssistantFetcher.download_console_assistant(aid, email=email, password=password, force_download=force_download) 82 | elif file: 83 | AssistantFetcher.copy_local_file(file) 84 | 85 | 86 | @staticmethod 87 | def download_public_assistant(url, force_download=False): 88 | message = pp.ConsoleMessage("Downloading assistant from {}".format(url)) 89 | message.start() 90 | 91 | if not force_download and AssistantFetcher.exists_cached_from_url(url): 92 | message.done() 93 | pp.psubsuccess("Using cached version from {}".format(SNIPS_CACHE_DIR)) 94 | AssistantFetcher.copy_to_temp_assistant_from_url(url) 95 | return 96 | 97 | try: 98 | content = fetch_url(url) 99 | message.done() 100 | except: 101 | raise AssistantFetcherException("Error downloading assistant. Please make sure the assistant URL is correct") 102 | 103 | filepath = AssistantFetcher.get_assistant_cache_path_from_url(url) 104 | message = pp.ConsoleMessage("Saving assistant to {}".format(SNIPS_CACHE_DIR)) 105 | message.start() 106 | write_binary_file(filepath, content) 107 | message.done() 108 | AssistantFetcher.copy_to_temp_assistant_from_url(url) 109 | 110 | 111 | @staticmethod 112 | def download_console_assistant(aid, email=None, password=None, force_download=False): 113 | start_message = "Fetching assistant $GREEN{}$RESET from the Snips Console".format(aid) 114 | 115 | if not force_download and AssistantFetcher.exists_cached_from_assistant_id(aid): 116 | pp.psubsuccess(start_message) 117 | pp.psubsuccess("Using cached version: {}".format(AssistantFetcher.get_assistant_cache_path_from_assistant_id(aid))) 118 | AssistantFetcher.copy_to_temp_assistant_from_assistant_id(aid) 119 | return 120 | 121 | token = Cache.get_login_token() 122 | if token is None: 123 | token = AssistantFetcher.get_token(email=email, password=password) 124 | 125 | message = pp.ConsoleMessage(start_message) 126 | message.start() 127 | try: 128 | content = AssistantFetcher.download_console_assistant_only(aid, token) 129 | message.done() 130 | except Exception as e: 131 | message.error() 132 | Logout.logout() 133 | token = AssistantFetcher.get_token(email=email, password=password) 134 | message = pp.ConsoleMessage("Retrying to fetch assistant $GREEN{}$RESET from the Snips Console".format(aid)) 135 | message.start() 136 | try: 137 | content = AssistantFetcher.download_console_assistant_only(aid, token) 138 | message.done() 139 | except Exception: 140 | message.error() 141 | raise AssistantFetcherException("Error fetching assistant from the console. Please make sure the ID is correct, and that you are signed in") 142 | 143 | filepath = AssistantFetcher.get_assistant_cache_path_from_assistant_id(aid) 144 | message = pp.ConsoleMessage("Saving assistant to {}".format(filepath)) 145 | message.start() 146 | write_binary_file(filepath, content) 147 | message.done() 148 | AssistantFetcher.copy_to_temp_assistant_from_assistant_id(aid) 149 | 150 | 151 | @staticmethod 152 | def download_console_assistant_only(aid, token): 153 | url = AssistantFetcher.CONSOLE_ASSISTANT_URL.format(aid) 154 | return fetch_url(url, headers={'Authorization': token, 'Accept': 'application/json'}) 155 | 156 | 157 | @staticmethod 158 | def exists_cached_from_url(url): 159 | return file_exists(AssistantFetcher.get_assistant_cache_path_from_url(url)) 160 | 161 | 162 | @staticmethod 163 | def exists_cached_from_assistant_id(assistant_id): 164 | return file_exists(AssistantFetcher.get_assistant_cache_path_from_assistant_id(assistant_id)) 165 | 166 | 167 | @staticmethod 168 | def exists_assistant_filename(filename): 169 | return file_exists(AssistantFetcher.get_assistant_file_path(filename)) 170 | 171 | 172 | @staticmethod 173 | def get_assistant_filename_from_url(url): 174 | return "assistant_" + hashlib.sha224(url).hexdigest() + ".zip" 175 | 176 | 177 | @staticmethod 178 | def get_assistant_filename_from_assistant_id(assistant_id): 179 | return "assistant_{}.zip".format(assistant_id) 180 | 181 | 182 | @staticmethod 183 | def get_assistant_cache_path_from_url(url): 184 | return AssistantFetcher.get_assistant_file_path(AssistantFetcher.get_assistant_filename_from_url(url)) 185 | 186 | 187 | @staticmethod 188 | def get_assistant_cache_path_from_assistant_id(assistant_id): 189 | return AssistantFetcher.get_assistant_file_path(AssistantFetcher.get_assistant_filename_from_assistant_id(assistant_id)) 190 | 191 | 192 | @staticmethod 193 | def get_assistant_file_path(filename): 194 | return os.path.join(SNIPS_CACHE_DIR, filename) 195 | 196 | 197 | @staticmethod 198 | def get_token(email=None, password=None): 199 | try: 200 | return Login.login(email=email, password=password, greeting="Please enter your Snips Console credentials to download your assistant.", silent=True) 201 | except Exception as e: 202 | raise AssistantFetcherException("Error logging in: {}".format(str(e))) 203 | 204 | 205 | @staticmethod 206 | def copy_to_temp_assistant_from_url(url): 207 | AssistantFetcher.copy_local_file(AssistantFetcher.get_assistant_cache_path_from_url(url), silent=True) 208 | 209 | 210 | @staticmethod 211 | def copy_to_temp_assistant_from_assistant_id(assistant_id): 212 | AssistantFetcher.copy_local_file(AssistantFetcher.get_assistant_cache_path_from_assistant_id(assistant_id), silent=True) 213 | 214 | 215 | @staticmethod 216 | def copy_local_file(file_path, silent=False): 217 | if not silent: 218 | message = pp.ConsoleMessage("Copying assistant {} to {}".format(file_path, AssistantFetcher.SNIPS_TEMP_ASSISTANT_PATH)) 219 | message.start() 220 | 221 | error = None 222 | if not file_exists(file_path): 223 | error = "Error: failed to locate file {}".format(file_path) 224 | else: 225 | try: 226 | shutil.copy2(file_path, AssistantFetcher.SNIPS_TEMP_ASSISTANT_PATH) 227 | except Exception as e: 228 | error = "Error: failed to copy file {}. Make sure you have write permissions to {}".format(file_path, AssistantFetcher.SNIPS_TEMP_ASSISTANT_PATH) 229 | 230 | if error is not None: 231 | if not silent: 232 | message.error() 233 | raise AssistantFetcherException(error) 234 | else: 235 | if not silent: 236 | message.done() 237 | -------------------------------------------------------------------------------- /snipsmanager/commands/assistant/load.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | 3 | import os 4 | import shutil 5 | import time 6 | 7 | from ..base import Base 8 | from ..session.login import Login 9 | from ...utils.http_helpers import fetch_url 10 | from ...utils.os_helpers import write_binary_file, file_exists, is_raspi_os 11 | from ...utils.cache import Cache 12 | from ...utils.snips import Snips 13 | from ...utils.intent_class_generator import IntentClassGenerator 14 | from .fetch import AssistantFetcher 15 | 16 | from ... import SNIPS_CACHE_INTENTS_DIR 17 | 18 | from snipsmanagercore import pretty_printer as pp 19 | 20 | 21 | class AssistantLoaderException(Exception): 22 | pass 23 | 24 | 25 | # pylint: disable=too-few-public-methods 26 | class AssistantLoader(Base): 27 | 28 | def run(self): 29 | """ Command runner. 30 | 31 | Docopt command: 32 | 33 | snipsmanager load assistant [--file= --platform-only] 34 | """ 35 | try: 36 | generate_classes = not self.options['--platform-only'] 37 | AssistantLoader.load(self.options['--file'], generate_classes=generate_classes) 38 | except Exception as e: 39 | pp.perror(str(e)) 40 | 41 | 42 | @staticmethod 43 | def load(file_path=None, generate_classes=True): 44 | pp.pcommand("Loading assistant") 45 | 46 | if file_path is None: 47 | file_path = AssistantFetcher.SNIPS_TEMP_ASSISTANT_PATH 48 | 49 | message = pp.ConsoleMessage("Loading assistant from file {}".format(file_path)) 50 | message.start() 51 | 52 | if file_path is not None and not file_exists(file_path): 53 | message.error() 54 | raise AssistantLoaderException("Error loading assistant: file {} not found".format(file_path)) 55 | 56 | if is_raspi_os(): 57 | if not Snips.is_installed(): 58 | message.error() 59 | raise AssistantLoaderException("Error: loading an assistant requires the Snips platform to be installed. Please run 'curl https://install.snips.ai -sSf | sh' to install the Snips Platform") 60 | Snips.load_assistant(file_path) 61 | 62 | message.done() 63 | 64 | if generate_classes: 65 | AssistantLoader.generate_intent_classes(file_path) 66 | 67 | pp.psuccess("Assistant has been successfully loaded") 68 | 69 | @staticmethod 70 | def generate_intent_classes(file_path): 71 | message = pp.ConsoleMessage("Generating classes from assistant model") 72 | message.start() 73 | try: 74 | shutil.rmtree(SNIPS_CACHE_INTENTS_DIR) 75 | except Exception: 76 | pass 77 | IntentClassGenerator().generate(file_path, SNIPS_CACHE_INTENTS_DIR) 78 | message.done() 79 | -------------------------------------------------------------------------------- /snipsmanager/commands/base.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ The base command. """ 3 | 4 | # SNIPSFILE = "Snipsfile" 5 | # ASSISTANT_DIR = ".snips" 6 | # ASSISTANT_ZIP_FILENAME = "assistant.zip" 7 | # ASSISTANT_ZIP_PATH = "{}/{}".format(ASSISTANT_DIR, ASSISTANT_ZIP_FILENAME) 8 | # INTENTS_DIR = ".snips/intents" 9 | 10 | # pylint: disable=too-few-public-methods 11 | class Base(object): 12 | """ The base command. """ 13 | 14 | def __init__(self, options, *args, **kwargs): 15 | """ Initialisation. 16 | 17 | :param options: command-line options. 18 | :param *args, **kwargs: extra arguments. 19 | """ 20 | self.options = options 21 | self.args = args 22 | self.kwargs = kwargs 23 | self.snipsfile = None 24 | 25 | def run(self): 26 | """ Command runner. """ 27 | raise NotImplementedError( 28 | 'You must implement the run() method yourself!') 29 | -------------------------------------------------------------------------------- /snipsmanager/commands/install/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/snipsmanager/commands/install/__init__.py -------------------------------------------------------------------------------- /snipsmanager/commands/install/addon.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | 3 | import os 4 | 5 | from ..base import Base 6 | from ...utils.addons import Addons 7 | from ...utils.os_helpers import ask_for_input 8 | 9 | from snipsmanagercore import pretty_printer as pp 10 | 11 | class AddonInstallerException(Exception): 12 | pass 13 | 14 | class AddonInstallerWarning(Exception): 15 | pass 16 | 17 | 18 | class AddonInstaller(Base): 19 | 20 | SPOTIFY_LOGIN_URL = "https://snips-spotify-login.herokuapp.com" 21 | 22 | def run(self): 23 | pp.silent = self.options['--silent'] 24 | interactive = not self.options['--non-interactive'] 25 | addon_id = self.options[''] 26 | try: 27 | if addon_id == "spotify": 28 | AddonInstaller.install_spotify_addon(params=self.options['PARAMS'], interactive=interactive) 29 | else: 30 | raise AddonInstallerException("Error: Unknown add-on {}".format(addon_id)) 31 | except AddonInstallerWarning as e: 32 | pp.pwarning(str(e)) 33 | except Exception as e: 34 | pp.perror(str(e)) 35 | 36 | 37 | @staticmethod 38 | def install_spotify_addon(params=None, interactive=True): 39 | pp.pcommand("Installing Spotify add-on") 40 | 41 | if params is None or len(params) == 0: 42 | if interactive: 43 | pp.psubmessage("You need to provide a Spotify token", indent=True) 44 | pp.psubmessage("Please open \033[1m\033[4m{}\033[0m in a web browser and follow the instructions to obtain it".format(AddonInstaller.SPOTIFY_LOGIN_URL), indent=True) 45 | token = ask_for_input("Spotify token:") 46 | else: 47 | pp.pwarning("Spotify add-on not installed. Please provide the Spotify token as a parameter, or omit the `--non-interative` flag") 48 | return 49 | else: 50 | token = params[0] 51 | 52 | Addons.install("spotify", [token]) 53 | 54 | pp.psuccess("Spotify add-on installed") -------------------------------------------------------------------------------- /snipsmanager/commands/install/bluetooth.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | 3 | import os 4 | 5 | from ..base import Base 6 | from ...utils.os_helpers import is_raspi_os, is_node_available, execute_command, download_file, file_exists 7 | 8 | from ... import prepare_cache, NODE_MODULES_PARENT_DIR, NODE_MODULES_DIR 9 | 10 | from snipsmanagercore import pretty_printer as pp 11 | 12 | class BluetoothInstallerException(Exception): 13 | pass 14 | 15 | 16 | class BluetoothInstaller(Base): 17 | 18 | SNIPS_MQTT_RELAY_MODULE_NAME = "snips-mqtt-relay" 19 | 20 | def run(self): 21 | try: 22 | BluetoothInstaller.install(force_download=self.options['--force-download']) 23 | except Exception as e: 24 | pp.perror(str(e)) 25 | 26 | 27 | @staticmethod 28 | def install(force_download=False): 29 | pp.pcommand("Setting up Bluetooth") 30 | 31 | if not is_raspi_os(): 32 | raise BluetoothInstallerException("Error: Bluetooth is only available on Raspberry Pi") 33 | 34 | if not is_node_available(): 35 | BluetoothInstaller.install_node() 36 | 37 | if force_download or not BluetoothInstaller.is_snips_mqtt_relay_installed(): 38 | message = pp.ConsoleMessage("Installing Node module $GREEN{}$RESET".format(BluetoothInstaller.SNIPS_MQTT_RELAY_MODULE_NAME)) 39 | message.start() 40 | try: 41 | execute_command("sudo npm install --no-cache --prefix={} {}".format(NODE_MODULES_PARENT_DIR, BluetoothInstaller.SNIPS_MQTT_RELAY_MODULE_NAME), True) 42 | message.done() 43 | except: 44 | message.error() 45 | raise BluetoothInstallerException("Error: Error installing Bluetooth module {}. Please install it manually".format(BluetoothInstaller.SNIPS_MQTT_RELAY_MODULE_NAME)) 46 | 47 | pp.psuccess("Bluetooth is successfully installed") 48 | 49 | 50 | @staticmethod 51 | def is_snips_mqtt_relay_installed(): 52 | return os.path.isdir(os.path.join(NODE_MODULES_DIR, BluetoothInstaller.SNIPS_MQTT_RELAY_MODULE_NAME)) 53 | 54 | 55 | @staticmethod 56 | def install_node(): 57 | pp.psubmessage("Node is not available. Installing node.", indent=True) 58 | message = pp.ConsoleMessage("Removing previous version of Node") 59 | message.start() 60 | try: 61 | execute_command("sudo apt-get -y remove nodejs nodejs-legacy npm") 62 | message.done() 63 | except: 64 | message.error() 65 | pass 66 | 67 | 68 | deb_file = "node_latest_armhf.deb" 69 | deb_url = "http://node-arm.herokuapp.com/node_latest_armhf.deb" 70 | 71 | message = pp.ConsoleMessage("Downloading Raspbian-compatible version of Node") 72 | message.start() 73 | 74 | try: 75 | download_file(deb_url, deb_file) 76 | message.done() 77 | except: 78 | message.error() 79 | raise BluetoothInstallerException("Error: failed to download compatible version of Node") 80 | 81 | 82 | message = pp.ConsoleMessage("Installing Node") 83 | message.start() 84 | try: 85 | execute_command("sudo dpkg -i {}".format(deb_file)) 86 | message.done() 87 | except: 88 | message.error() 89 | raise BluetoothInstallerException("Error: installing Node. Please install Node manually") 90 | 91 | try: 92 | remove_file(filename) 93 | except: 94 | pass 95 | 96 | pp.psuccess("Successfully installed Node") 97 | -------------------------------------------------------------------------------- /snipsmanager/commands/install/install.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | 3 | import os 4 | 5 | from ..base import Base 6 | from ...utils.os_helpers import file_exists 7 | from ...utils.os_helpers import is_raspi_os 8 | from ...utils.snipsfile import Snipsfile 9 | 10 | from ..assistant.fetch import AssistantFetcher 11 | from ..assistant.load import AssistantLoader 12 | from ..setup.microphone import MicrophoneInstaller 13 | from ..setup.speaker import SpeakerInstaller 14 | from ..setup.systemd.bluetooth import SystemdBluetooth 15 | from ..setup.systemd.snipsmanager import SystemdSnipsManager 16 | from .skills import SkillsInstaller, SkillsInstallerWarning 17 | from .bluetooth import BluetoothInstaller 18 | 19 | from ... import DEFAULT_SNIPSFILE_PATH 20 | 21 | from snipsmanagercore import pretty_printer as pp 22 | 23 | 24 | class GlobalInstallerException(Exception): 25 | pass 26 | 27 | 28 | class GlobalInstallerWarning(Exception): 29 | pass 30 | 31 | 32 | class GlobalInstaller(Base): 33 | 34 | def run(self): 35 | pp.silent = self.options['--silent'] 36 | debug = self.options['--debug'] 37 | try: 38 | GlobalInstaller.install(self.options['--snipsfile'], skip_bluetooth=self.options['--skip-bluetooth'], skip_systemd=self.options['--skip-systemd'], email=self.options['--email'], password=self.options['--password'], force_download=self.options['--force-download']) 39 | except GlobalInstallerWarning as e: 40 | if debug: 41 | raise e 42 | pp.pwarning(str(e)) 43 | except Exception as e: 44 | if debug: 45 | raise e 46 | pp.perror(str(e)) 47 | 48 | 49 | @staticmethod 50 | def install(snipsfile_path=None, skip_bluetooth=False, skip_systemd=False, email=None, password=None, force_download=False): 51 | snipsfile_path = snipsfile_path or DEFAULT_SNIPSFILE_PATH 52 | if snipsfile_path is not None and not file_exists(snipsfile_path): 53 | raise GlobalInstallerException("Error running installer: Snipsfile not found") 54 | snipsfile = Snipsfile(snipsfile_path) 55 | GlobalInstaller.install_from_snipsfile(snipsfile, skip_bluetooth=skip_bluetooth, skip_systemd=skip_systemd, email=email, password=password, force_download=force_download) 56 | 57 | 58 | @staticmethod 59 | def install_from_snipsfile(snipsfile, skip_bluetooth=False, skip_systemd=False, email=None, password=None, force_download=False): 60 | pp.pheader("Running Snips Manager installer") 61 | 62 | if snipsfile is None: 63 | raise GlobalInstallerException("Error running installer: no Snipsfile provided") 64 | 65 | try: 66 | AssistantFetcher.fetch(email=email, password=password, force_download=force_download) 67 | AssistantLoader.load() 68 | except Exception as e: 69 | pp.pwarning(str(e)) 70 | 71 | try: 72 | SkillsInstaller.install(force_download=force_download) 73 | except Exception as e: 74 | pp.pwarning(str(e)) 75 | 76 | try: 77 | MicrophoneInstaller.install() 78 | except Exception as e: 79 | pp.pwarning(str(e)) 80 | 81 | try: 82 | SpeakerInstaller.install() 83 | except Exception as e: 84 | pp.pwarning(str(e)) 85 | 86 | if not skip_bluetooth and is_raspi_os(): 87 | try: 88 | BluetoothInstaller.install(force_download=force_download) 89 | except Exception as e: 90 | pp.pwarning(str(e)) 91 | 92 | if not skip_systemd and is_raspi_os(): 93 | try: 94 | SystemdBluetooth.setup() 95 | except Exception as e: 96 | pp.pwarning(str(e)) 97 | try: 98 | SystemdSnipsManager.setup() 99 | except Exception as e: 100 | pp.pwarning(str(e)) 101 | 102 | if not skip_systemd: 103 | pp.pheadersuccess("Snips Manager installer complete! You can now reboot your device, or manually run 'snipsmanager run' to start the Snips Manager server") 104 | else: 105 | pp.pheadersuccess("Snips Manager installer complete! Now run 'snipsmanager run' to start the Snips Manager server.") 106 | -------------------------------------------------------------------------------- /snipsmanager/commands/install/skill.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | 3 | import os 4 | 5 | from ..base import Base 6 | from ...utils.pip_installer import PipInstaller 7 | 8 | from snipsmanagercore import pretty_printer as pp 9 | 10 | class SkillInstallerException(Exception): 11 | pass 12 | 13 | class SkillInstallerWarning(Exception): 14 | pass 15 | 16 | 17 | class SkillInstaller(Base): 18 | 19 | def run(self): 20 | url_or_pip = self.options[''] 21 | force_download = self.options['--force-download'] 22 | debug = self.options['--debug'] 23 | 24 | try: 25 | SkillInstaller.install(url_or_pip, force_download=force_download, debug=debug) 26 | except SkillInstallerWarning as e: 27 | if debug: 28 | raise e 29 | pp.pwarning(str(e)) 30 | except Exception as e: 31 | if debug: 32 | raise e 33 | pp.perror(str(e)) 34 | 35 | 36 | @staticmethod 37 | def install(url_or_pip, force_download=False, debug=False): 38 | message = pp.ConsoleMessage("Installing skill: $GREEN{}$RESET".format(url_or_pip)) 39 | message.start() 40 | try: 41 | PipInstaller.install(url_or_pip, force_download=force_download) 42 | message.done() 43 | except Exception as e: 44 | message.error() 45 | if debug: 46 | raise e 47 | 48 | raise SkillInstallerWarning("Error installing skill {}: make sure you have the required access rights, and that the module is available".format(url_or_pip)) 49 | -------------------------------------------------------------------------------- /snipsmanager/commands/install/skills.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | 3 | import os 4 | 5 | from ..base import Base 6 | from ...utils.pip_installer import PipInstaller 7 | from ...utils.os_helpers import file_exists 8 | from ...utils.snipsfile import Snipsfile 9 | from .skill import SkillInstaller, SkillInstallerWarning 10 | 11 | from ... import DEFAULT_SNIPSFILE_PATH 12 | 13 | from snipsmanagercore import pretty_printer as pp 14 | 15 | class SkillsInstallerException(Exception): 16 | pass 17 | 18 | class SkillsInstallerWarning(Exception): 19 | pass 20 | 21 | 22 | class SkillsInstaller(Base): 23 | 24 | def run(self): 25 | try: 26 | SkillsInstaller.install(snipsfile_path=self.options['--snipsfile'], silent=self.options['--silent'], force_download=self.options['--force-download']) 27 | except SkillsInstallerWarning as e: 28 | pp.pwarning(str(e)) 29 | except Exception as e: 30 | pp.perror(str(e)) 31 | 32 | 33 | @staticmethod 34 | def install(snipsfile_path=None, silent=False, force_download=False): 35 | SkillsInstaller.print_start(silent) 36 | if snipsfile_path is None: 37 | snipsfile_path = DEFAULT_SNIPSFILE_PATH 38 | if snipsfile_path is not None and not file_exists(snipsfile_path): 39 | raise SkillsInstallerException("Error installing skills: Snipsfile not found") 40 | snipsfile = Snipsfile(snipsfile_path) 41 | num_installed = SkillsInstaller.install_from_snipsfile(snipsfile, silent=True, force_download=force_download) 42 | SkillsInstaller.print_done(num_installed, silent) 43 | 44 | 45 | @staticmethod 46 | def install_from_snipsfile(snipsfile, silent=False, force_download=False): 47 | SkillsInstaller.print_start(silent) 48 | if snipsfile is None: 49 | raise SkillsInstallerException("Error installing skills: no Snipsfile provided") 50 | skill_urls = snipsfile.get_skill_urls() 51 | num_skills_without_url = snipsfile.get_num_skills_without_url() 52 | if len(skill_urls) + num_skills_without_url == 0: 53 | raise SkillsInstallerWarning("No skills found in Snipsfile. Skipping skill installation") 54 | num_installed = SkillsInstaller.install_from_urls(skill_urls, silent=True, force_download=force_download) 55 | SkillsInstaller.print_done(num_installed + num_skills_without_url, silent) 56 | return num_installed + num_skills_without_url 57 | 58 | 59 | @staticmethod 60 | def install_from_urls(skill_urls, silent=False, force_download=False): 61 | SkillsInstaller.print_start(silent) 62 | num_installed = 0 63 | for url in skill_urls: 64 | try: 65 | SkillInstaller.install(url, force_download=force_download) 66 | num_installed = num_installed + 1 67 | except SkillInstallerWarning as e: 68 | pp.pwarning(str(e)) 69 | except Exception as e: 70 | pp.perror(str(e)) 71 | SkillsInstaller.print_done(num_installed, silent) 72 | return num_installed 73 | 74 | 75 | @staticmethod 76 | def print_start(silent=False): 77 | if not silent: 78 | pp.pcommand("Installing skills") 79 | 80 | 81 | @staticmethod 82 | def print_done(num_installed, silent=False): 83 | if not silent: 84 | if num_installed == 1: 85 | pp.psuccess("Successfully installed 1 skill") 86 | else: 87 | pp.psuccess("Successfully installed {} skills".format(num_installed)) 88 | -------------------------------------------------------------------------------- /snipsmanager/commands/scaffold.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | import os 3 | from jinja2 import Environment, PackageLoader 4 | 5 | from .base import Base 6 | from ..utils.os_helpers import create_dir_verbose, write_text_file_verbose, ask_yes_no, ask_for_input, \ 7 | get_user_email_git, email_is_valid 8 | from ..utils.wizard import Wizard 9 | 10 | 11 | # pylint: disable=too-few-public-methods 12 | class Scaffold(Base): 13 | """ 14 | The scaffold command. 15 | 16 | The goal is to generate the following structure for a Snips skill 17 | 18 | /[projectname]/ 19 | /[projectname]/setup.py 20 | /[projectname]/README.rst 21 | /[projectname]/LICENSE.txt 22 | /[projectname]/MANIFEST.in 23 | /[projectname]/lint.cfg 24 | /[projectname]/setup.cfg 25 | /[projectname]/[projectname] 26 | /[projectname]/[projectname]/__init__.py 27 | /[projectname]/[projectname]/Snipsspec 28 | /[projectname]/tests 29 | /[projectname]/tests/__init__.py 30 | /[projectname]/tests/[projectname]_tests.py 31 | """ 32 | 33 | def __init__(self): 34 | self.jinja_env = Environment( 35 | loader=PackageLoader('snipsmanager', 'templates')) 36 | 37 | self.wizard = Wizard() 38 | self.wizard.add_question( 39 | description="Give your skill a name. For instance: lightskill, gardeningskill, etc ...", 40 | text="Project name? ", 41 | input_function=ask_for_input, 42 | input_validation=lambda x: len(x) > 0) 43 | self.wizard.add_question(description="A short sentence to describe what your skill does.", 44 | text="Description? ", 45 | input_function=ask_for_input, 46 | input_validation=lambda x: len(x) > 0) 47 | self.wizard.add_question(description="", 48 | text="Author? ", 49 | input_function=ask_for_input, 50 | input_validation=lambda x: True) 51 | self.wizard.add_question(description="", 52 | text="Email address? ", 53 | input_function=ask_for_input, 54 | input_validation=email_is_valid, 55 | default_value=get_user_email_git()) 56 | 57 | def run(self): 58 | current_directory = os.getcwd() 59 | 60 | project_name, description, author, email = [question.answer() for question in self.wizard] 61 | print "\n" 62 | 63 | try: 64 | # log("Scaffolding {} structure".format(project_name)) 65 | self.create_folders(current_directory, project_name) 66 | self.create_files(current_directory=current_directory, project_name=project_name, description=description, 67 | author=author, email=email) 68 | 69 | except IOError as e: 70 | pass 71 | # log_error(e.strerror) 72 | 73 | def retrieve_user_information(self): 74 | """ 75 | The templates need the following variables from the user : 76 | - project_name 77 | - pypi identifier 78 | - description 79 | - author 80 | - email 81 | - github_url 82 | """ 83 | 84 | def create_folders(self, current_directory, project_name): 85 | root_directory = os.path.join(current_directory, project_name) 86 | 87 | if os.path.exists(root_directory) and os.listdir(root_directory): 88 | raise IOError() 89 | else: 90 | create_dir_verbose(root_directory, 0) 91 | 92 | directory_names = (project_name, 'tests') 93 | 94 | for directory in directory_names: 95 | directory_name = os.path.join(root_directory, directory) 96 | create_dir_verbose(directory_name, 1) 97 | 98 | def create_files(self, current_directory, project_name, description, author, email): 99 | root_directory = os.path.join(current_directory, project_name) 100 | 101 | self.write_setup(root_directory, project_name, email, description, author) 102 | self.write_snips_spec(project_name, root_directory) 103 | self.write_inits(project_name, root_directory) 104 | self.write_unit_tests(project_name, root_directory) 105 | self.write_readme(project_name, root_directory) 106 | self.write_manifest(project_name, root_directory) 107 | self.write_configs(project_name, root_directory) 108 | 109 | def write_setup(self, root_directory, project_name, email, description, author): 110 | setup_path = os.path.join(root_directory, 'setup.py') 111 | 112 | SETUP_template = self.jinja_env.get_template('setup.py') 113 | setup_content = SETUP_template.render(project_name=project_name, email=email, description=description, 114 | author=author) 115 | write_text_file_verbose(setup_path, setup_content, 1) 116 | 117 | def write_snips_spec(self, project_name, root_directory): 118 | spec_path = os.path.join(root_directory, project_name, 'Snipsspec') 119 | 120 | spec_content = '' 121 | write_text_file_verbose(spec_path, spec_content, 2) 122 | 123 | def write_inits(self, project_name, root_directory): 124 | project_init = os.path.join(root_directory, project_name, '__init__.py') 125 | tests_init = os.path.join(root_directory, 'tests', '__init__.py') 126 | 127 | write_text_file_verbose(project_init, '', 2) 128 | write_text_file_verbose(tests_init, '', 2) 129 | 130 | def write_unit_tests(self, project_name, root_directory): 131 | pass 132 | 133 | def write_readme(self, project_name, root_directory): 134 | readme_path = os.path.join(root_directory, 'README.rst') 135 | 136 | README_template = self.jinja_env.get_template('README.rst') 137 | README_content = README_template.render(project_name=project_name) 138 | write_text_file_verbose(readme_path, README_content, 1) 139 | 140 | def write_license(self, project_name, root_directory): 141 | license_path = os.path.join(root_directory, 'LICENSE.txt') 142 | LICENSE_template = self.jinja_env.get_template('LICENSE.txt') 143 | 144 | write_text_file_verbose(license_path, LICENSE_template.render(), 1) 145 | 146 | def write_manifest(self, project_name, root_directory): 147 | manifest_path = os.path.join(root_directory, 'MANIFEST.in') 148 | 149 | MANIFEST_template = self.jinja_env.get_template('MANIFEST.in') 150 | manifest_content = MANIFEST_template.render(project_name=project_name) 151 | write_text_file_verbose(manifest_path, manifest_content, 1) 152 | 153 | def write_configs(self, project_name, root_directory): 154 | lint_path = os.path.join(root_directory, 'lint.cfg') 155 | setup_path = os.path.join(root_directory, 'setup.cfg') 156 | 157 | LINT_template = self.jinja_env.get_template('lint.cfg') 158 | SETUP_CONFIG_template = self.jinja_env.get_template('setup.cfg') 159 | 160 | lint_content = LINT_template.render() 161 | setup_content = SETUP_CONFIG_template.render() 162 | 163 | write_text_file_verbose(lint_path, lint_content, 1) 164 | write_text_file_verbose(setup_path, setup_content, 1) 165 | -------------------------------------------------------------------------------- /snipsmanager/commands/session/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/snipsmanager/commands/session/__init__.py -------------------------------------------------------------------------------- /snipsmanager/commands/session/login.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | 3 | from ..base import Base 4 | from .logout import Logout 5 | from snipsmanagercore import pretty_printer as pp 6 | from ...utils.os_helpers import write_text_file, read_file, file_exists, ask_for_input, ask_for_password 7 | from ...utils.cache import Cache 8 | from ...utils.auth import Auth 9 | 10 | 11 | class InvalidTokenException(Exception): 12 | pass 13 | 14 | 15 | class Login(Base): 16 | 17 | def run(self): 18 | try: 19 | Login.login(email=self.options['--email'], password=self.options['--password']) 20 | except Exception as e: 21 | pp.perror("Error logging in: {}".format(str(e))) 22 | 23 | 24 | @staticmethod 25 | def login(email=None, password=None, greeting=None, silent=False): 26 | has_credentials = email is not None and password is not None 27 | silent = silent or has_credentials 28 | if has_credentials: 29 | Logout.logout() 30 | token = Cache.get_login_token() 31 | if not token: 32 | if not has_credentials: 33 | pp.pcommand(greeting or "Please enter your Snips Console credentials") 34 | email = ask_for_input("Email address:") 35 | password = ask_for_password("Password:") 36 | token = Auth.retrieve_token(email, password) 37 | if token is not None: 38 | Cache.save_login_token(token) 39 | if not silent: 40 | pp.psuccess("You are now signed in") 41 | else: 42 | raise InvalidTokenException("Could not validate authentication token") 43 | else: 44 | if not silent: 45 | pp.psuccess("You are already signed in") 46 | return token 47 | -------------------------------------------------------------------------------- /snipsmanager/commands/session/logout.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | 3 | from ..base import Base 4 | from snipsmanagercore import pretty_printer as pp 5 | from ...utils.os_helpers import write_text_file, read_file, file_exists, ask_for_input, ask_for_password 6 | from ...utils.cache import Cache 7 | from ...utils.auth import Auth 8 | 9 | # pylint: disable=too-few-public-methods 10 | class Logout(Base): 11 | 12 | def run(self): 13 | if Cache.get_login_token() is not None: 14 | Logout.logout() 15 | pp.psuccess("You are now signed out") 16 | else: 17 | pp.psuccess("You are already signed out") 18 | 19 | @staticmethod 20 | def logout(): 21 | Cache.clear_login_token() 22 | -------------------------------------------------------------------------------- /snipsmanager/commands/setup/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/snipsmanager/commands/setup/__init__.py -------------------------------------------------------------------------------- /snipsmanager/commands/setup/microphone.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """The microphone setup command.""" 3 | 4 | import time 5 | 6 | from ..base import Base 7 | from ...utils.os_helpers import file_exists 8 | from ...utils.os_helpers import is_raspi_os 9 | from ...utils.microphone_setup import MicrophoneSetup, RespeakerMicrophoneSetup 10 | from ...utils.snipsfile import Snipsfile 11 | 12 | from ... import DEFAULT_SNIPSFILE_PATH, ASOUNDCONF_DEST_PATH 13 | 14 | from snipsmanagercore import pretty_printer as pp 15 | 16 | class MicrophoneInstallerException(Exception): 17 | pass 18 | 19 | class MicrophoneInstallerWarning(Exception): 20 | pass 21 | 22 | 23 | # pylint: disable=too-few-public-methods 24 | class MicrophoneInstaller(Base): 25 | """The microphone setup command.""" 26 | 27 | def run(self): 28 | """ Command runner. 29 | 30 | Docopt command: 31 | 32 | snipsmanager setup microphone [--snipsfile=] [ [--skip-asoundconf] [PARAMS ...]] 33 | """ 34 | try: 35 | microphone_id = self.options[''] 36 | if microphone_id is not None: 37 | update_asoundconf = not self.options['--skip-asoundconf'] 38 | params = self.options['PARAMS'] 39 | MicrophoneInstaller.install_from_params(microphone_id, update_asoundconf, params_list=params) 40 | else: 41 | MicrophoneInstaller.install(snipsfile_path=self.options['--snipsfile']) 42 | except MicrophoneInstallerWarning as e: 43 | pp.pwarning(str(e)) 44 | except Exception as e: 45 | pp.perror(str(e)) 46 | 47 | @staticmethod 48 | def install(snipsfile_path=None, silent=False): 49 | MicrophoneInstaller.print_start(silent=silent) 50 | if snipsfile_path is None: 51 | snipsfile_path = DEFAULT_SNIPSFILE_PATH 52 | if snipsfile_path is not None and not file_exists(snipsfile_path): 53 | raise SkillsInstallerException("Error setting up microphone: Snipsfile not found") 54 | snipsfile = Snipsfile(snipsfile_path) 55 | MicrophoneInstaller.install_from_snipsfile(snipsfile, silent=True) 56 | MicrophoneInstaller.print_done(silent) 57 | 58 | 59 | @staticmethod 60 | def install_from_snipsfile(snipsfile, silent=False): 61 | MicrophoneInstaller.print_start(silent=silent) 62 | if snipsfile is None: 63 | raise MicrophoneInstallerException("Error setting up microphone: Snipsfile not found") 64 | 65 | microphone_id = snipsfile.microphone_config.identifier 66 | params_dict = snipsfile.microphone_config.params 67 | modify_asoundconf = snipsfile.modify_asoundconf 68 | 69 | MicrophoneInstaller.install_from_params(microphone_id, modify_asoundconf, params_dict=params_dict, silent=True) 70 | MicrophoneInstaller.print_done(silent) 71 | 72 | 73 | @staticmethod 74 | def install_from_params(microphone_id, update_asoundconf, params_list=None, params_dict=None, silent=False): 75 | MicrophoneInstaller.print_start(microphone_id, silent) 76 | 77 | if not is_raspi_os(): 78 | raise MicrophoneInstallerWarning("System is not Raspberry Pi. Skipping microphone setup") 79 | 80 | if update_asoundconf: 81 | message = pp.ConsoleMessage("Copying asound configuration to {}".format(ASOUNDCONF_DEST_PATH)) 82 | message.start() 83 | MicrophoneSetup.setup_asoundconf(microphone_id) 84 | message.done() 85 | 86 | if microphone_id == 'respeaker': 87 | message = pp.ConsoleMessage("Installing ReSpeaker drivers") 88 | message.start() 89 | try: 90 | respeaker_params = MicrophoneInstaller.normalize_respeaker_params(params_list=params_list, params_dict=params_dict) 91 | RespeakerMicrophoneSetup.setup(respeaker_params['vendorId'], respeaker_params['productId']) 92 | message.done() 93 | except MicrophoneInstallerException as e: 94 | message.error() 95 | raise MicrophoneInstallerException(str(e)) 96 | 97 | MicrophoneInstaller.print_done(silent) 98 | 99 | 100 | @staticmethod 101 | def normalize_respeaker_params(params_list=None, params_dict=None): 102 | vendor_id = None 103 | product_id = None 104 | exception = MicrophoneInstallerException("Error installing ReSpeaker drivers: you must provide both vendor id and product id") 105 | if params_list is not None: 106 | if len(params_list) < 2: 107 | raise exception 108 | vendor_id = params_list[0] 109 | product_id = params_list[1] 110 | elif params_dict is not None: 111 | if not 'vendor_id' in params_dict and not 'product_id' in params_dict: 112 | raise exception 113 | vendorId = params_dict['vendor_id'] 114 | productId = params_dict['product_id'] 115 | return { 'vendorId': vendorId, 'productId': productId } 116 | 117 | 118 | @staticmethod 119 | def print_start(microphone_id=None, silent=False): 120 | if not silent: 121 | if microphone_id is not None: 122 | pp.pcommand("Setting up microphone: {}".format(microphone_id)) 123 | else: 124 | pp.pcommand("Setting up microphone") 125 | 126 | 127 | @staticmethod 128 | def print_done(silent=False): 129 | if not silent: 130 | pp.psuccess("Microphone setup successfully complete") 131 | -------------------------------------------------------------------------------- /snipsmanager/commands/setup/speaker.py: -------------------------------------------------------------------------------- 1 | 2 | from ..base import Base 3 | from ...utils.os_helpers import file_exists 4 | from ...utils.os_helpers import is_raspi_os 5 | from snipsmanagercore import pretty_printer as pp 6 | from ...utils.snipsfile import Snipsfile 7 | from ...utils.speaker_setup import SpeakerSetup 8 | from ... import DEFAULT_SNIPSFILE_PATH, ASOUNDCONF_DEST_PATH 9 | 10 | 11 | class SpeakerInstallerException(Exception): 12 | pass 13 | 14 | 15 | class SpeakerInstallerWarning(Exception): 16 | pass 17 | 18 | 19 | class SpeakerInstaller(Base): 20 | """Speaker setup command""" 21 | 22 | def run(self): 23 | 24 | try: 25 | speaker_id = self.options[''] 26 | if speaker_id is not None: 27 | update_asoundconf = not self.options['--skip-asoundconf'] 28 | params = self.options['PARAMS'] 29 | SpeakerInstaller.install_from_params(speaker_id, update_asoundconf, params_list=params) 30 | else: 31 | SpeakerInstaller.install(snipsfile_path=self.options['--snipsfile']) 32 | except SpeakerInstallerWarning as e: 33 | pp.pwarning(str(e)) 34 | except Exception as e: 35 | pp.perror(str(e)) 36 | 37 | 38 | @staticmethod 39 | def install(snipsfile_path=None, silent=False): 40 | SpeakerInstaller.print_start(silent=silent) 41 | if snipsfile_path is None: 42 | snipsfile_path = DEFAULT_SNIPSFILE_PATH 43 | if snipsfile_path is not None and not file_exists(snipsfile_path): 44 | raise SkillsInstallerException("Error setting up speaker: Snipsfile not found") 45 | snipsfile = Snipsfile(snipsfile_path) 46 | SpeakerInstaller.install_from_snipsfile(snipsfile, silent=True) 47 | SpeakerInstaller.print_done(silent) 48 | 49 | @staticmethod 50 | def install_from_snipsfile(snipsfile, silent=False): 51 | SpeakerInstaller.print_start(silent=silent) 52 | if snipsfile is None: 53 | raise SpeakerInstallerException("Error setting up speaker: Snipsfile not found") 54 | 55 | speaker_id = snipsfile.speaker_config.identifier 56 | params_dict = snipsfile.speaker_config.params 57 | modify_asoundconf = snipsfile.speaker_config.modify_asoundconf 58 | 59 | SpeakerInstaller.install_from_params(speaker_id, modify_asoundconf, params_dict=params_dict, silent=True) 60 | SpeakerInstaller.print_done(silent) 61 | 62 | @staticmethod 63 | def install_from_params(speaker_id, update_asoundconf, params_list=None, params_dict=None, silent=False): 64 | SpeakerInstaller.print_start(speaker_id, silent) 65 | 66 | if not is_raspi_os(): 67 | raise SpeakerInstallerWarning("System is not Raspberry Pi. Skipping speaker setup") 68 | 69 | message = pp.ConsoleMessage("Installing driver") 70 | message.start() 71 | SpeakerSetup.setup_driver(speaker_id) 72 | message.done() 73 | 74 | if update_asoundconf: 75 | message = pp.ConsoleMessage("Copying asound.conf to {}".format(ASOUNDCONF_DEST_PATH)) 76 | message.start() 77 | SpeakerSetup.setup_asoundconf(speaker_id) 78 | message.done() 79 | 80 | SpeakerInstaller.print_done(silent) 81 | 82 | 83 | @staticmethod 84 | def print_start(speaker_id=None, silent=False): 85 | if not silent: 86 | if speaker_id is not None: 87 | pp.pcommand("Setting up speaker: {}".format(speaker_id)) 88 | else: 89 | pp.pcommand("Setting up speaker") 90 | 91 | 92 | @staticmethod 93 | def print_done(silent=False): 94 | if not silent: 95 | pp.psuccess("Speaker setup successfully complete") 96 | -------------------------------------------------------------------------------- /snipsmanager/commands/setup/systemd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/snipsmanager/commands/setup/systemd/__init__.py -------------------------------------------------------------------------------- /snipsmanager/commands/setup/systemd/bluetooth.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """The microphone setup command.""" 3 | 4 | import time 5 | 6 | from ...base import Base 7 | from ....utils.os_helpers import is_raspi_os, is_node_available, which, file_exists 8 | from ....utils.systemd import Systemd 9 | from ....utils.snipsfile import Snipsfile 10 | 11 | from .... import NODE_MODULES_DIR, DEFAULT_SNIPSFILE_PATH 12 | 13 | from snipsmanagercore import pretty_printer as pp 14 | 15 | class SystemdBluetoothException(Exception): 16 | pass 17 | 18 | 19 | class SystemdBluetooth(Base): 20 | 21 | SNIPSBLE_SERVICE_NAME = "snipsble" 22 | SNIPSBLE_SERVICE_UUID = "13EA4259-9D9E-42D1-A78B-638ED22CC768" 23 | SNIPSBLE_CHARACTERISTIC_UUID = "81D97A06-7A2D-4A98-A2E2-41688E3D8283" 24 | SNIPSBLE_MODULE_NAME = "snips-mqtt-relay" 25 | SNIPSBLE_SCRIPT = "{node_bin_path} {node_module_path}/{module_name}/index.js --serviceUUID={serviceUUID} --characteristicUUID={characteristicUUID} {mqtt_hostname} {mqtt_port}" 26 | 27 | def run(self): 28 | try: 29 | mqtt_hostname = self.options['--mqtt-host'] 30 | mqtt_port = self.options['--mqtt-port'] 31 | if mqtt_hostname is not None or mqtt_port is not None: 32 | SystemdBluetooth.setup_from_params(mqtt_hostname=mqtt_hostname, mqtt_port=mqtt_port) 33 | else: 34 | SystemdBluetooth.setup(self.options['--snipsfile']) 35 | except Exception as e: 36 | pp.perror(str(e)) 37 | 38 | 39 | @staticmethod 40 | def setup(snipsfile_path=None): 41 | if snipsfile_path is None: 42 | snipsfile_path = DEFAULT_SNIPSFILE_PATH 43 | if snipsfile_path is not None and not file_exists(snipsfile_path): 44 | raise SystemdBluetoothException("Error setting up Bluetooth systemd: Snipsfile not found") 45 | snipsfile = Snipsfile(snipsfile_path) 46 | SystemdBluetooth.setup_from_snipsfile(snipsfile) 47 | 48 | 49 | @staticmethod 50 | def setup_from_snipsfile(snipsfile): 51 | if snipsfile is None: 52 | raise SystemdBluetoothException("Error setting up Bluetooth systemd: Snipsfile not found") 53 | SystemdBluetooth.setup_from_params(snipsfile.mqtt_hostname, snipsfile.mqtt_port) 54 | 55 | 56 | @staticmethod 57 | def setup_from_params(mqtt_hostname="localhost", mqtt_port=1883): 58 | pp.pcommand("Setting up Bluetooth as a Systemd service") 59 | 60 | if not is_raspi_os(): 61 | raise SystemdBluetoothException("Bluetooth Systemd configuration is only available on Raspberry Pi. Skipping Systemd setup") 62 | 63 | if not is_node_available(): 64 | raise SystemdBluetoothException("Error: Bluetooth module must be installed. Run 'snipsmanager install bluetooth' to setup Bluetooth") 65 | 66 | contents = Systemd.get_template(SystemdBluetooth.SNIPSBLE_SERVICE_NAME) 67 | if contents is None: 68 | return 69 | 70 | node_bin_path = which('node') 71 | command = SystemdBluetooth.SNIPSBLE_SCRIPT.format( 72 | node_bin_path=node_bin_path, 73 | node_module_path=NODE_MODULES_DIR, 74 | module_name=SystemdBluetooth.SNIPSBLE_MODULE_NAME, 75 | serviceUUID=SystemdBluetooth.SNIPSBLE_SERVICE_UUID, 76 | characteristicUUID=SystemdBluetooth.SNIPSBLE_CHARACTERISTIC_UUID, 77 | mqtt_hostname=mqtt_hostname, 78 | mqtt_port=mqtt_port 79 | ) 80 | 81 | contents = contents.replace("{{SNIPSBLE_COMMAND}}", command) 82 | Systemd.write_systemd_file(SystemdBluetooth.SNIPSBLE_SERVICE_NAME, None, contents) 83 | Systemd.enable_service(None, SystemdBluetooth.SNIPSBLE_SERVICE_NAME) 84 | 85 | pp.psuccess("Successfully set up Bluetooth as a Systemd service") 86 | -------------------------------------------------------------------------------- /snipsmanager/commands/setup/systemd/snipsmanager.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | 3 | import os 4 | import time 5 | 6 | from ...base import Base 7 | from ....utils.os_helpers import is_raspi_os, which 8 | from ....utils.systemd import Systemd 9 | 10 | from .... import DEFAULT_SNIPSFILE_PATH 11 | 12 | from snipsmanagercore import pretty_printer as pp 13 | 14 | class SystemdSnipsManagerException(Exception): 15 | pass 16 | 17 | 18 | class SystemdSnipsManager(Base): 19 | 20 | SNIPSMANAGER_SERVICE_NAME = "snipsmanager" 21 | SNIPSMANAGER_COMMAND = "snipsmanager" 22 | 23 | def run(self): 24 | snipsfile_path = self.options['--snipsfile_path'] or os.getcwd() 25 | try: 26 | SystemdSnipsManager.setup(snipsfile_path=snipsfile_path) 27 | except Exception as e: 28 | pp.perror(str(e)) 29 | 30 | 31 | @staticmethod 32 | def setup(snipsfile_path=None): 33 | pp.pcommand("Setting up Snips Manager as a Systemd service") 34 | 35 | snipsfile_path = snipsfile_path or DEFAULT_SNIPSFILE_PATH 36 | working_directory = os.path.dirname(snipsfile_path) 37 | 38 | if not is_raspi_os(): 39 | raise SystemdSnipsManagerException("Snips Systemd configuration is only available on Raspberry Pi. Skipping Systemd setup") 40 | 41 | snipsmanager_path = which('snipsmanager') 42 | if snipsmanager_path is None: 43 | raise SystemdSnipsManagerException("Error: cannot find command 'snipsmanager' on the system. Make sure the Snips Manager CLI is correctly installed. Skipping Systemd setup") 44 | 45 | contents = Systemd.get_template(SystemdSnipsManager.SNIPSMANAGER_SERVICE_NAME) 46 | contents = contents.replace("{{SNIPSMANAGER_COMMAND}}", snipsmanager_path) 47 | contents = contents.replace("{{WORKING_DIRECTORY}}", working_directory) 48 | Systemd.write_systemd_file(SystemdSnipsManager.SNIPSMANAGER_SERVICE_NAME, None, contents) 49 | Systemd.enable_service(None, SystemdSnipsManager.SNIPSMANAGER_SERVICE_NAME) 50 | 51 | pp.psuccess("Successfully set up Snips Manager as a Systemd service") 52 | -------------------------------------------------------------------------------- /snipsmanager/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/snipsmanager/config/__init__.py -------------------------------------------------------------------------------- /snipsmanager/config/asound.conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/snipsmanager/config/asound.conf/__init__.py -------------------------------------------------------------------------------- /snipsmanager/config/asound.conf/asound.conf.default: -------------------------------------------------------------------------------- 1 | pcm.!default { 2 | type asym 3 | playback.pcm { 4 | type plug 5 | slave.pcm "hw:0,0" 6 | } 7 | capture.pcm { 8 | type plug 9 | slave.pcm "hw:1,0" 10 | } 11 | } -------------------------------------------------------------------------------- /snipsmanager/config/asound.conf/asound.conf.jabra: -------------------------------------------------------------------------------- 1 | pcm.!default { 2 | type asym 3 | playback.pcm { 4 | type plug 5 | slave.pcm "hw:1,0" 6 | } 7 | capture.pcm { 8 | type plug 9 | slave.pcm "hw:1,0" 10 | } 11 | } -------------------------------------------------------------------------------- /snipsmanager/config/asound.conf/asound.conf.respeaker: -------------------------------------------------------------------------------- 1 | pcm.!default { 2 | type asym 3 | playback.pcm { 4 | type plug 5 | slave.pcm "hw:0,0" 6 | } 7 | capture.pcm { 8 | type dsnoop 9 | ipc_key 5432 10 | slave { 11 | pcm "hw:1,0" 12 | buffer_size 96000 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /snipsmanager/config/asound.conf/asound.conf.speakerbonnet: -------------------------------------------------------------------------------- 1 | pcm.speakerbonnet { 2 | type hw card 0 3 | } 4 | 5 | pcm.!default { 6 | type plug 7 | slave.pcm "dmixer" 8 | } 9 | 10 | pcm.dmixer { 11 | type dmix 12 | ipc_key 1024 13 | ipc_perm 0666 14 | slave { 15 | pcm "speakerbonnet" 16 | period_time 0 17 | period_size 1024 18 | buffer_size 8192 19 | rate 44100 20 | channels 2 21 | } 22 | } 23 | 24 | ctl.dmixer { 25 | type hw card 0 26 | } -------------------------------------------------------------------------------- /snipsmanager/config/asound.conf/asoundrc.speakerbonnet: -------------------------------------------------------------------------------- 1 | pcm.speakerbonnet { 2 | type hw card 0 3 | } 4 | 5 | pcm.dmixer { 6 | type dmix 7 | ipc_key 1024 8 | ipc_perm 0666 9 | slave { 10 | pcm "speakerbonnet" 11 | period_time 0 12 | period_size 1024 13 | buffer_size 8192 14 | rate 44100 15 | channels 2 16 | } 17 | } 18 | 19 | ctl.dmixer { 20 | type hw card 0 21 | } 22 | 23 | pcm.softvol { 24 | type softvol 25 | slave.pcm "dmixer" 26 | control.name "PCM" 27 | control.card 0 28 | } 29 | 30 | ctl.softvol { 31 | type hw card 0 32 | } 33 | 34 | pcm.!default { 35 | type plug 36 | slave.pcm "softvol" 37 | } -------------------------------------------------------------------------------- /snipsmanager/config/drivers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/snipsmanager/config/drivers/__init__.py -------------------------------------------------------------------------------- /snipsmanager/config/systemd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/snipsmanager/config/systemd/__init__.py -------------------------------------------------------------------------------- /snipsmanager/config/systemd/snipsble.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Snips BLE 3 | After=snips.service 4 | 5 | [Service] 6 | Type=simple 7 | ExecStartPre=/bin/sleep 30 8 | ExecStart={{SNIPSBLE_COMMAND}} 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /snipsmanager/config/systemd/snipsmanager.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Snips Manager 3 | After=snips.service 4 | 5 | [Service] 6 | Type=simple 7 | User=%i 8 | ExecStartPre=/bin/sleep 30 9 | WorkingDirectory={{WORKING_DIRECTORY}} 10 | ExecStart={{SNIPSMANAGER_COMMAND}} run 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /snipsmanager/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/snipsmanager/models/__init__.py -------------------------------------------------------------------------------- /snipsmanager/models/dialoguedef.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Dialogue definition from a YAML config. """ 3 | 4 | # pylint: disable=too-few-public-methods 5 | class DialogueDef: 6 | """ Dialogue definition from a YAML config. """ 7 | 8 | def __init__(self, name, action): 9 | """ Initialisation. 10 | 11 | :param name: the name of the dialogue event. 12 | :param action: the code to execute. 13 | """ 14 | self.name = name 15 | self.action = action 16 | -------------------------------------------------------------------------------- /snipsmanager/models/intentdef.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Intent definition from a YAML config. """ 3 | 4 | # pylint: disable=too-few-public-methods 5 | class IntentDef: 6 | """ Intent definition from a YAML config. """ 7 | 8 | def __init__(self, name, action): 9 | """ Initialisation. 10 | 11 | :param name: the name of the intent. 12 | :param action: the code to execute. 13 | """ 14 | self.name = name 15 | self.action = action 16 | -------------------------------------------------------------------------------- /snipsmanager/models/notificationdef.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Notification definition from a YAML config. """ 3 | 4 | # pylint: disable=too-few-public-methods 5 | class NotificationDef: 6 | """ Notification definition from a YAML config. """ 7 | 8 | def __init__(self, name, action): 9 | """ Initialisation. 10 | 11 | :param name: the name of the notification. 12 | :param action: the code to execute. 13 | """ 14 | self.name = name 15 | self.action = action 16 | -------------------------------------------------------------------------------- /snipsmanager/models/skilldef.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Skill definition from a YAML config. """ 3 | 4 | # pylint: disable=too-few-public-methods 5 | class SkillDef: 6 | """ Skill definition from a YAML config. """ 7 | 8 | # pylint: disable=too-many-arguments 9 | def __init__(self, name, package_name, class_name, url, params, intent_defs, dialogue_events_defs, notification_defs, requires_tts, addons): 10 | """ Initialisation. 11 | 12 | :param name: skill name. 13 | :param package_name: the name of the Python module. 14 | :param class_name: the name of the Python class. 15 | :param url: the url package (name or url). 16 | :param params: the parameters to pass to the skills constructor. 17 | :param intent_defs: a list of intent definitions. 18 | :param dialogue_events_defs: a list of notification definitions. 19 | :param notification_defs: a list of notification definitions. 20 | :param requires_tts: whether the skill requires TTS. 21 | :param addons: addon modules. 22 | """ 23 | self.name = name 24 | self.package_name = package_name 25 | self.class_name = class_name 26 | self.url = url 27 | self.params = params 28 | self.intent_defs = intent_defs 29 | self.dialogue_events_defs = dialogue_events_defs 30 | self.notification_defs = notification_defs 31 | self.requires_tts = requires_tts 32 | self.addons = addons 33 | 34 | def find(self, intent): 35 | """ Find an intent definition in the list of intents that the skill 36 | declares. 37 | 38 | :param intent: the intent object to look for. 39 | :return: an intent definition, from the skill definition, if found, 40 | or None. 41 | """ 42 | if intent is None: 43 | return None 44 | for intent_def in self.intent_defs: 45 | if intent_def.name == intent.intentName: 46 | return intent_def 47 | return None 48 | 49 | def find_wildcard(self): 50 | """ Find a wildcard intent definition, i.e. one with name "*". 51 | 52 | :return: an wildcard intent definition, from the skill definition, 53 | if found, or None. 54 | """ 55 | for intent_def in self.intent_defs: 56 | if intent_def.name == "*": 57 | return intent_def 58 | return None 59 | 60 | 61 | def find_notification(self, name): 62 | """ Find a notification definition in the list of notifications 63 | that the skill declares. 64 | 65 | :param name: the name of the notification object to look for. 66 | :return: a notification definition, from the skill definition, 67 | if found, or None. 68 | """ 69 | for notification_def in self.notification_defs: 70 | if notification_def.name == name: 71 | return notification_def 72 | return None 73 | 74 | def find_dialogue_event(self, name): 75 | """ Find a dialogue event definition in the list of dialogue events 76 | that the skill declares. 77 | 78 | :param name: the name of the dialogue event object to look for. 79 | :return: a dialogue event definition, from the skill definition, 80 | if found, or None. 81 | """ 82 | for dialogue_event_def in self.dialogue_events_defs: 83 | if dialogue_event_def.name == name: 84 | return dialogue_event_def 85 | return None 86 | -------------------------------------------------------------------------------- /snipsmanager/snipsmanager: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # DO NOT REMOVE! 4 | # This file is used for the Debian package. 5 | 6 | /opt/venvs/snipsmanager/bin/snipsmanager "$@" -------------------------------------------------------------------------------- /snipsmanager/templates/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Snips 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /snipsmanager/templates/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include {{project_name}}/Snipsspec -------------------------------------------------------------------------------- /snipsmanager/templates/README.rst: -------------------------------------------------------------------------------- 1 | {{project_name}} skill for Snips 2 | ====================================== 3 | 4 | |MIT License| 5 | 6 | {{project_name}} 7 | 8 | Installation 9 | ------------ 10 | 11 | Usage 12 | ----- 13 | Snips Manager 14 | ^^^^^^^^^^^^^ 15 | 16 | It is recommended that you use this skill with the `Snips Manager `_. Simply add the following section to your `Snipsfile `_: 17 | 18 | Standalone usage 19 | ^^^^^^^^^^^^^^^^ 20 | 21 | Contributing 22 | ------------ 23 | 24 | Please see the `Contribution Guidelines`_. 25 | 26 | .. |MIT License| image:: https://img.shields.io/badge/license-MIT-blue.svg 27 | :target: https://raw.githubusercontent.com/snipsco/snips-skill-hue/master/LICENSE.txt 28 | :alt: MIT License 29 | 30 | .. _`pip`: http://www.pip-installer.org 31 | .. _`Snips`: https://www.snips.ai 32 | .. _`LICENSE.txt`: https://github.com/snipsco/snips-skill-hue/blob/master/LICENSE.txt 33 | .. _`Contribution Guidelines`: https://github.com/snipsco/snips-skill-hue/blob/master/CONTRIBUTING.rst 34 | .. _snipsmanager: https://github.com/snipsco/snipsmanager -------------------------------------------------------------------------------- /snipsmanager/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/snipsmanager/templates/__init__.py -------------------------------------------------------------------------------- /snipsmanager/templates/intent_registry_template.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Class for holding all the intent classes present in the assistant. """ 3 | 4 | # ***************************************************************************** 5 | # ***************************************************************************** 6 | # ***************************************************************************** 7 | # 8 | # WARNING: THIS IS AN AUTO-GENERATED FILE 9 | # DO NOT ATTEMPT TO EDIT IT, AS CHANGES WILL BE OVERWRITTEN. 10 | # 11 | # ***************************************************************************** 12 | # ***************************************************************************** 13 | # ***************************************************************************** 14 | 15 | # pylint: disable=line-too-long 16 | 17 | {% for intent in intents -%} 18 | from intents.{{camel_case_to_underscore(to_camelcase_capitalized(intent.name))}}_intent import {{to_camelcase_capitalized(intent.name)}}Intent 19 | {% endfor %} 20 | 21 | class IntentRegistry: 22 | """ Class for holding all the intent classes present in the assistant. """ 23 | 24 | # pylint: disable=too-few-public-methods 25 | def __init__(self): 26 | """ Initialisation. """ 27 | self.intent_classes = [ 28 | {% for intent in intents -%} 29 | {{to_camelcase_capitalized(intent.name)}}Intent{{"," if not loop.last}} 30 | {% endfor -%} 31 | ] 32 | 33 | -------------------------------------------------------------------------------- /snipsmanager/templates/intent_template.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Auto-generated intent class. """ 3 | 4 | # ***************************************************************************** 5 | # ***************************************************************************** 6 | # ***************************************************************************** 7 | # 8 | # WARNING: THIS IS AN AUTO-GENERATED FILE 9 | # DO NOT ATTEMPT TO EDIT IT, AS CHANGES WILL BE OVERWRITTEN. 10 | # 11 | # ***************************************************************************** 12 | # ***************************************************************************** 13 | # ***************************************************************************** 14 | 15 | # pylint: disable=line-too-long 16 | 17 | from snipsmanagercore.intent_parser import IntentParser 18 | 19 | class {{to_camelcase_capitalized(intent.name)}}Intent: 20 | 21 | intentName = "{{ intent.name }}" 22 | 23 | def __init__(self, sessionId, siteId, customData{% for slot in intent.slots -%}, {{slot.name}}=None{% endfor %}): 24 | self.sessionId = sessionId 25 | self.siteId = siteId 26 | self.customData = customData 27 | {% for slot in intent.slots -%} 28 | self.{{slot.name}} = {{slot.name}} 29 | {% endfor %} 30 | 31 | @staticmethod 32 | def parse(payload): 33 | intentName = IntentParser.get_intent_name(payload) 34 | if intentName != {{to_camelcase_capitalized(intent.name)}}Intent.intentName: 35 | return None 36 | return {{to_camelcase_capitalized(intent.name)}}Intent( 37 | IntentParser.get_session_id(payload), 38 | IntentParser.get_site_id(payload), 39 | IntentParser.get_custom_data(payload), 40 | {% for slot in intent.slots -%} 41 | IntentParser.get_slot_value(payload, "{{ slot.name }}"){{"," if not loop.last}} 42 | {% endfor -%} 43 | ) 44 | -------------------------------------------------------------------------------- /snipsmanager/templates/lint.cfg: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=too-few-public-methods -------------------------------------------------------------------------------- /snipsmanager/templates/setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /snipsmanager/templates/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='', 5 | version='0.0.1', 6 | description='{{description}}', 7 | author='{{author}}', 8 | author_email='{{email}}', 9 | download_url='', 10 | license='MIT', 11 | install_requires=[], 12 | setup_requires=['green'], 13 | keywords=['snips'], 14 | include_package_data=True, 15 | packages=[ 16 | '{{project_name}}' 17 | ] 18 | ) -------------------------------------------------------------------------------- /snipsmanager/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snipsmanager/9eb45c076db4ed90e1a5a7cdeadfda253affaadb/snipsmanager/utils/__init__.py -------------------------------------------------------------------------------- /snipsmanager/utils/addons.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Addon handler. """ 3 | 4 | import os 5 | 6 | from .os_helpers import read_file 7 | from .. import SNIPS_CACHE_DIR 8 | 9 | class Addons: 10 | 11 | SPOTIFY_ENV_KEY = 'spotify_refresh_token' 12 | 13 | @staticmethod 14 | def install(addon_id, params): 15 | if addon_id == "spotify" and len(params) > 0: 16 | EnvCache.set_env(Addons.SPOTIFY_ENV_KEY, params[0]) 17 | 18 | @staticmethod 19 | def update_params(params, addon_id): 20 | if addon_id == "spotify": 21 | value = EnvCache.get_env(Addons.SPOTIFY_ENV_KEY) 22 | if value is not None: 23 | params[Addons.SPOTIFY_ENV_KEY] = value 24 | return True 25 | return False 26 | return False 27 | 28 | class EnvCache: 29 | 30 | STORE_FILE = os.path.join(SNIPS_CACHE_DIR, "env_cache") 31 | 32 | @staticmethod 33 | def get_env(key): 34 | cache = read_file(EnvCache.STORE_FILE) 35 | if cache is None: 36 | return None 37 | for line in cache.splitlines(): 38 | if line.startswith(key + "="): 39 | return "=".join(line.split("=")[1:]) 40 | return None 41 | 42 | @staticmethod 43 | def remove_env(key): 44 | cache = read_file(EnvCache.STORE_FILE) 45 | if cache is None: 46 | return 47 | filtered = [] 48 | for line in cache.splitlines(): 49 | if not line.startswith(key + "="): 50 | filtered.append(line) 51 | cache = "\n".join(filtered) 52 | EnvCache.save(cache) 53 | 54 | @staticmethod 55 | def set_env(key, value): 56 | EnvCache.remove_env(key) 57 | line = key + "=" + value + "\n" 58 | cache = read_file(EnvCache.STORE_FILE) 59 | if cache is None: 60 | cache = "" 61 | cache = cache + line 62 | EnvCache.save(cache) 63 | 64 | @staticmethod 65 | def save(cache): 66 | with open(EnvCache.STORE_FILE, "w") as f: 67 | f.write(cache) 68 | -------------------------------------------------------------------------------- /snipsmanager/utils/assistant_downloader.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Downloader for Snips assistants. """ 3 | 4 | from http_helpers import post_request_json 5 | import os 6 | import json 7 | import re 8 | 9 | from ..utils.os_helpers import email_is_valid 10 | 11 | try: 12 | from urllib.request import urlopen 13 | except ImportError: 14 | from urllib2 import urlopen, Request, URLError 15 | 16 | USER_AUTH_ROUTE = "https://external-gateway.snips.ai/v1/user/auth" 17 | 18 | class Auth: 19 | 20 | @staticmethod 21 | def retrieve_token(self, email, password): 22 | data = { 'email': email, 'password': password } 23 | response, response_headers = post_request_json(AUTH_URL, data) 24 | token = response_headers.getheader('Authorization') 25 | return token 26 | 27 | 28 | class AuthException(Exception): 29 | pass 30 | 31 | 32 | class AuthExceptionInvalidCredentials(AuthException): 33 | pass 34 | 35 | 36 | class AuthExceptionInvalidAssistantId(AuthException): 37 | pass 38 | 39 | 40 | class DownloaderException(Exception): 41 | """ Exceptions related to downloads of Snips assistants. """ 42 | pass 43 | 44 | 45 | # pylint: disable=too-few-public-methods 46 | class Downloader(object): 47 | """ Downloader for Snips assistants. """ 48 | 49 | @staticmethod 50 | def download(url, output_dir, filename): 51 | """ Download a file, and save it to a file. 52 | 53 | :param url: the URL of the file. 54 | :param output_dir: the directory where the file should be 55 | saved. 56 | """ 57 | try: 58 | response = urlopen(url) 59 | except Exception: 60 | raise DownloaderException() 61 | 62 | Downloader.save(response.read(), 63 | output_dir, 64 | filename) 65 | 66 | @staticmethod 67 | def save(content, output_dir, filename): 68 | """ Save content of a file. 69 | 70 | :param content: the content of the file to save. 71 | :param output_dir: the directory where the assistant should be 72 | saved. 73 | """ 74 | if not os.path.exists(output_dir): 75 | os.makedirs(output_dir) 76 | output_filename = "{}/{}".format(output_dir, filename) 77 | with open(output_filename, "wb") as output_file: 78 | output_file.write(content) 79 | 80 | 81 | class AuthDownloader(Downloader): 82 | 83 | def __init__(self, email, password, assistantId): 84 | self.email = email 85 | self.password = password 86 | self.assistant_id = assistantId 87 | self.validate_input() 88 | 89 | 90 | @property 91 | def auth_url(self): 92 | raise NotImplementedError 93 | 94 | @property 95 | def download_url(self): 96 | raise NotImplementedError 97 | 98 | def validate_input(self): 99 | if not email_is_valid(self.email): 100 | raise AuthExceptionInvalidCredentials("Error, Email is not valid") 101 | 102 | if len(self.password) < 1: 103 | raise AuthExceptionInvalidCredentials("Error, password is too short") 104 | 105 | if len(self.assistant_id) < 14: 106 | raise AuthExceptionInvalidAssistantId("Error, assistantId is too short") 107 | 108 | def retrieve_auth_token(self): 109 | data = {'email': self.email, 'password': self.password} 110 | 111 | try: 112 | response, response_headers = post_request_json(self.auth_url, data) 113 | token = response_headers.getheader('Authorization') 114 | return token 115 | except URLError: 116 | raise DownloaderException 117 | 118 | def download(self, output_dir, filename): 119 | try: 120 | token = self.retrieve_auth_token() 121 | request = Request(self.download_url, headers={'Authorization': token, 'Accept': 'application/json'}) 122 | response = urlopen(request) 123 | except Exception: 124 | raise DownloaderException() 125 | 126 | Downloader.save(response.read(), 127 | output_dir, 128 | filename) 129 | 130 | 131 | class AssistantDownloader(AuthDownloader): 132 | auth_url = "https://external-gateway.snips.ai/v1/user/auth" 133 | download_url = "https://external-gateway.snips.ai/v1/assistant/{}/download" 134 | 135 | def __init__(self, email, password, assistantId): 136 | AuthDownloader.__init__(self, email, password, assistantId) 137 | self.download_url = self.download_url.format(self.assistant_id) 138 | -------------------------------------------------------------------------------- /snipsmanager/utils/auth.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Downloader for Snips assistants. """ 3 | 4 | from http_helpers import post_request_json 5 | 6 | class Auth: 7 | 8 | AUTH_URL = "https://external-gateway.snips.ai/v1/user/auth" 9 | 10 | @staticmethod 11 | def retrieve_token(email, password): 12 | data = { 'email': email, 'password': password } 13 | response, response_headers = post_request_json(Auth.AUTH_URL, data) 14 | token = response_headers.getheader('Authorization') 15 | return token 16 | -------------------------------------------------------------------------------- /snipsmanager/utils/cache.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Simple caching. """ 3 | 4 | import os 5 | from .os_helpers import write_text_file, read_file, create_dir, remove_file 6 | 7 | from .. import SNIPS_CACHE_DIR 8 | 9 | class Cache: 10 | 11 | STORE_FILE = os.path.join(SNIPS_CACHE_DIR, "token_store") 12 | 13 | @staticmethod 14 | def get_login_token(): 15 | return read_file(Cache.STORE_FILE) 16 | 17 | @staticmethod 18 | def save_login_token(token): 19 | write_text_file(Cache.STORE_FILE, token) 20 | 21 | @staticmethod 22 | def clear_login_token(): 23 | remove_file(Cache.STORE_FILE) -------------------------------------------------------------------------------- /snipsmanager/utils/http_helpers.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ HTTP helpers. """ 3 | 4 | import json 5 | import urllib2 6 | 7 | try: 8 | from urllib.request import urlopen, Request 9 | except ImportError: 10 | from urllib2 import urlopen, Request 11 | 12 | def post_request(url, data, headers): 13 | """ 14 | :param url: 15 | :type url: basestring 16 | :param data: 17 | :type data: dict 18 | :param headers: 19 | :type headers: dict 20 | :return: 21 | :rtype: basestring 22 | """ 23 | raw_data = json.dumps(data) 24 | req = Request(url, raw_data, headers) 25 | f = urllib2.urlopen(req) 26 | info = f.info() 27 | response = f.read() 28 | f.close() 29 | return response, info 30 | 31 | def post_request_json(url, data, headers={}): 32 | """ 33 | 34 | :param url: 35 | :type url: basestring 36 | :param data: 37 | :type data: dict 38 | :param headers: 39 | :type headers: dict 40 | :return: 41 | :rtype: dict 42 | """ 43 | headers['Content-Type'] = 'application/json' 44 | headers['Accept'] = 'application/json' 45 | 46 | response, info = post_request(url, data, headers) 47 | return json.loads(response), info 48 | 49 | def fetch_url(url, headers=None): 50 | if headers is None: 51 | return urlopen(url).read() 52 | else: 53 | return urlopen(Request(url, headers=headers)).read() 54 | -------------------------------------------------------------------------------- /snipsmanager/utils/intent_class_generator.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Tools to automatically generate intent classes from an assistant 3 | definition. 4 | """ 5 | 6 | import os 7 | import re 8 | import json 9 | import zipfile 10 | 11 | from jinja2 import Environment, PackageLoader 12 | 13 | from .os_helpers import create_dir 14 | 15 | def camel_case_to_underscore(text): 16 | """ Convert camel-case to underscore. 17 | 18 | :param text: a text, potentially in camel-case format. 19 | :return: the text, converted to underscore format. 20 | """ 21 | underscored = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text) 22 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', underscored).lower() 23 | 24 | 25 | def to_camelcase_capitalized(text): 26 | """ Convert to camel-case and capitalize. 27 | 28 | :param text: a text, potentially with dashes and underscores. 29 | :return: the text, converted to cancel-case format, and capitalized. 30 | """ 31 | hyphens = re.sub(r'(?!^)-([a-zA-Z])', 32 | lambda m: m.group(1).upper(), text) 33 | underscores = re.sub(r'(?!^)_([a-zA-Z])', 34 | lambda m: m.group(1).upper(), hyphens) 35 | return underscores[:1].upper() + underscores[1:] 36 | 37 | 38 | def save_intent_file(output_dir, filename, text): 39 | """ Save a text string to a given file. 40 | 41 | :param filename: a file name. 42 | :param text: a text to save. 43 | """ 44 | create_dir(output_dir) 45 | create_dir("{}/intents".format(output_dir)) 46 | output_filename = "{}/intents/{}".format(output_dir, filename) 47 | with open(output_filename, "w") as output_file: 48 | output_file.write(text) 49 | 50 | 51 | class IntentClassGenerator: 52 | """ Tools to automatically generate intent classes from an assistant 53 | definition. 54 | """ 55 | 56 | def __init__(self): 57 | """ Initialisation. """ 58 | self.jinja_env = Environment( 59 | loader=PackageLoader('snipsmanager', 'templates')) 60 | 61 | def generate_intent_file(self, intent, output_dir): 62 | """ Given a JSON intent, generate the corresponding Python intent class 63 | file. 64 | 65 | :param intent: a JSON intent. 66 | """ 67 | template = self.jinja_env.get_template('intent_template.py') 68 | # pylint: disable=no-member 69 | file_content = template.render(intent=intent) 70 | filename = camel_case_to_underscore( 71 | to_camelcase_capitalized(intent["name"])) + "_intent.py" 72 | save_intent_file(output_dir, filename, file_content) 73 | 74 | def generate_intent_registry_file(self, intents, output_dir): 75 | """ Given a list of intents, generate an intents registry, which is 76 | a list of intent classes. 77 | 78 | :param intents: a list of intents. 79 | """ 80 | template = self.jinja_env.get_template('intent_registry_template.py') 81 | # pylint: disable=no-member 82 | file_content = template.render(intents=intents) 83 | with open("{}/intent_registry.py".format(output_dir), "w") as output_file: 84 | output_file.write(file_content) 85 | 86 | def generate(self, assistant_filename, output_dir): 87 | """ Generate intent classes from assistant.json specification. 88 | 89 | :param assistant_filename: path to the assistant zip file 90 | :param output_dir: directory to which the intents and registry should be 91 | written. 92 | """ 93 | self.jinja_env.globals.update( 94 | camel_case_to_underscore=camel_case_to_underscore) 95 | self.jinja_env.globals.update( 96 | to_camelcase_capitalized=to_camelcase_capitalized) 97 | 98 | intents = [] 99 | content = zipfile.ZipFile(assistant_filename).read( 100 | 'assistant/assistant.json') 101 | data = json.loads(content.decode('utf-8')) 102 | for intent in data["intents"]: 103 | self.generate_intent_file(intent, output_dir) 104 | intents.append(intent) 105 | 106 | self.generate_intent_registry_file(intents, output_dir) 107 | 108 | with open("{}/__init__.py".format(output_dir), "w") as output_file: 109 | output_file.write("") 110 | 111 | with open("{}/intents/__init__.py".format(output_dir), "w") as output_file: 112 | output_file.write("") 113 | -------------------------------------------------------------------------------- /snipsmanager/utils/microphone_setup.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Downloader for Snips assistants. """ 3 | 4 | import os 5 | import shutil 6 | 7 | from .os_helpers import cmd_exists, is_raspi_os, execute_command, pipe_commands 8 | 9 | from .. import ASOUNDCONF_DEST_PATH 10 | 11 | # pylint: disable=too-few-public-methods 12 | class MicrophoneSetup: 13 | """ Downloader for Snips assistants. """ 14 | 15 | ASOUNDCONF_PATH = "../config/asound.conf" 16 | 17 | @staticmethod 18 | def setup_asoundconf(microphone_id): 19 | if not is_raspi_os(): 20 | return 21 | if microphone_id == 'respeaker': 22 | MicrophoneSetup._copy_asoundconf("asound.conf.respeaker") 23 | elif microphone_id == 'jabra': 24 | MicrophoneSetup._copy_asoundconf("asound.conf.jabra") 25 | else: 26 | MicrophoneSetup._copy_asoundconf("asound.conf.default") 27 | 28 | 29 | @staticmethod 30 | def _copy_asoundconf(asoundconf_file): 31 | """ Copy asound.conf configuration to local path. 32 | 33 | :param asoundconf_file: the name of the asound.conf configuration, as 34 | present in the config folder. 35 | """ 36 | this_dir, this_filename = os.path.split(__file__) 37 | asoundconf_path = os.path.join(this_dir, MicrophoneSetup.ASOUNDCONF_PATH, asoundconf_file) 38 | shutil.copy2(asoundconf_path, ASOUNDCONF_DEST_PATH) 39 | 40 | 41 | class RespeakerMicrophoneSetup: 42 | 43 | @staticmethod 44 | def setup(vendor_id, product_id): 45 | if not is_raspi_os(): 46 | return 47 | 48 | execute_command("sudo rm -f /lib/udev/rules.d/50-rspk.rules") 49 | 50 | echo_command = ("echo ACTION==\"add\", SUBSYSTEMS==\"usb\", ATTRS{{idVendor}}==\"{}\", " + 51 | "ATTRS{{idProduct}}==\"{}\", MODE=\"660\", GROUP=\"plugdev\"") \ 52 | .format(vendor_id, product_id) 53 | tee_command = "sudo tee --append /lib/udev/rules.d/50-rspk.rules" 54 | pipe_commands(echo_command, tee_command, silent=True) 55 | 56 | execute_command("sudo adduser pi plugdev") 57 | execute_command("sudo udevadm control --reload") 58 | execute_command("sudo udevadm trigger") 59 | -------------------------------------------------------------------------------- /snipsmanager/utils/object_from_dict.py: -------------------------------------------------------------------------------- 1 | class ObjectFromDict(object): 2 | def __init__(self, dictionary): 3 | self.__dict__ = dictionary 4 | -------------------------------------------------------------------------------- /snipsmanager/utils/os_helpers.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Helper methods for OS related tasks. """ 3 | 4 | from getpass import getpass 5 | import os 6 | import platform 7 | import re 8 | import shlex 9 | import subprocess 10 | import urllib2 11 | 12 | from snipsmanagercore import pretty_printer as pp 13 | 14 | email_regex = r"[^@]+@[^@]+\.[^@]+" 15 | 16 | github_url_regex = re.compile( 17 | r'^(?:http|ftp|git)s?://' # http:// or https:// 18 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... 19 | r'localhost|' #localhost... 20 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip 21 | r'(?::\d+)?' # optional port 22 | r'(?:/?|[/?]\S+)$', re.IGNORECASE) 23 | 24 | def cmd_exists(cmd): 25 | """ Check if a command exists. 26 | 27 | :param cmd: the command to look for. 28 | :return: true if the command exists, false otherwise. 29 | """ 30 | return subprocess.call("type " + cmd, shell=True, 31 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 32 | 33 | 34 | def is_raspi_os(): 35 | """ Check if the current system is Raspberry. 36 | 37 | :return: true if the current system is Raspberry. 38 | """ 39 | return 'arm' in " ".join(os.uname()) 40 | 41 | 42 | def is_mac_os(): 43 | """ Check if the current system is OSX. 44 | 45 | :return: true if the current system is OSX. 46 | """ 47 | return 'Darwin' in platform.system() 48 | 49 | 50 | def is_node_available(): 51 | return cmd_exists('node') and cmd_exists('npm') 52 | 53 | 54 | def file_exists(file_path): 55 | return os.path.exists(file_path) 56 | 57 | 58 | def create_dir(dir_name): 59 | """ Create directory in the current working directory, if it does 60 | not exist already. 61 | 62 | :param dir_name: the name of the directory. 63 | """ 64 | if not os.path.exists(dir_name): 65 | os.makedirs(dir_name) 66 | 67 | 68 | def create_dir_verbose(dir_name, indentation_level): 69 | create_dir(dir_name) 70 | 71 | 72 | def write_text_file(output_file_path, text): 73 | with open(output_file_path, "w") as f: 74 | f.write(text) 75 | 76 | 77 | def write_binary_file(output_file_path, content): 78 | with open(output_file_path, "wb") as f: 79 | f.write(content) 80 | 81 | 82 | def read_file(file_path): 83 | if not file_exists(file_path): 84 | return None 85 | with open(file_path, "r") as f: 86 | return f.read() 87 | return None 88 | 89 | 90 | def write_text_file_verbose(output_file_path, text, indentation_level): 91 | write_text_file(output_file_path, text) 92 | 93 | 94 | def execute_command(command, silent=False): 95 | """ Execute a shell command. 96 | 97 | :param command: the command to execute. 98 | :param silent: if True, do not output anything to terminal. 99 | """ 100 | if silent: 101 | stdout = open(os.devnull, 'w') 102 | stderr = open(os.devnull, 'w') 103 | else: 104 | stdout = subprocess.PIPE 105 | stderr = subprocess.PIPE 106 | return subprocess.Popen(command.split(), stdout=stdout, stderr=stderr).communicate() 107 | 108 | 109 | def pipe_commands(first_command, second_command, silent): 110 | """ Execute piped commands: `first_command | second_command`. 111 | 112 | :param first_command: the first command to execute. 113 | :param second_command: the second command to execute. 114 | """ 115 | process1 = subprocess.Popen(first_command.split(), stdout=subprocess.PIPE) 116 | if silent: 117 | FNULL = open(os.devnull, 'w') 118 | process2 = subprocess.Popen( 119 | second_command.split(), stdin=process1.stdout, stdout=FNULL) 120 | else: 121 | process2 = subprocess.Popen( 122 | second_command.split(), stdin=process1.stdout) 123 | process1.stdout.close() 124 | process2.communicate() 125 | 126 | 127 | def remove_file(file_path): 128 | """ Delete a file. 129 | 130 | :param file_path: the path to the file. 131 | """ 132 | try: 133 | os.remove(file_path) 134 | except OSError: 135 | pass 136 | 137 | 138 | def download_file(url, output_file): 139 | """ Download a file. 140 | 141 | :param url: the remote location of the file. 142 | :param output_file: the file to write to. 143 | """ 144 | downloaded_file = urllib2.urlopen(url) 145 | with open(output_file, 'wb') as output: 146 | output.write(downloaded_file.read()) 147 | 148 | 149 | def ask_yes_no(question, default_value=None): 150 | """ Ask a yes/no question in the prompt. 151 | 152 | :param question: the question to ask. 153 | :return: true if the user answered yes (or empty), false otherwise 154 | """ 155 | if default_value is not None: 156 | return default_value 157 | 158 | answer = raw_input("{} [Y/n] ".format(question)) 159 | if answer is not None and answer.strip() != "" and answer.lower() != "y": 160 | return False 161 | return True 162 | 163 | 164 | def ask_for_input(question, default_value=None): 165 | if default_value and len(default_value) > 0: 166 | question = pp.generate_user_input_string("{} [{}] ".format(question, default_value)) 167 | answer = raw_input(question) 168 | if len(answer) == 0: # The user hit enter. 169 | answer = default_value 170 | else: 171 | question = pp.generate_user_input_string(question) 172 | answer = raw_input(question) 173 | 174 | if answer is not None and answer.strip() != "": 175 | return answer 176 | else: 177 | return None 178 | 179 | 180 | def ask_for_password(question): 181 | question = pp.generate_user_input_string(question) 182 | answer = getpass(question) 183 | if answer is not None and answer.strip() != "": 184 | return answer.strip() 185 | else: 186 | return None 187 | 188 | 189 | def which(command): 190 | """ Get full path for an executable. 191 | 192 | :param command: the executable command, e.g. 'node'. 193 | :return: the full path for the command, e.g. '/usr/local/bin/node'. 194 | """ 195 | try: 196 | return subprocess.check_output( 197 | ['which', command]).strip() 198 | except subprocess.CalledProcessError: 199 | return None 200 | 201 | 202 | def reboot(): 203 | """ Reboot the device.""" 204 | execute_command("sudo reboot") 205 | 206 | 207 | def get_os_name(): 208 | os_release = subprocess.check_output(['cat', '/etc/os-release']) 209 | for line in os_release.splitlines(): 210 | if line.startswith("PRETTY_NAME="): 211 | split = line.split("=") 212 | if len(split) > 1: 213 | os_version = split[1] 214 | return os_version.replace("\"", "") 215 | return None 216 | 217 | 218 | def get_revision(): 219 | process1 = subprocess.Popen('cat /proc/cpuinfo'.split(), stdout=subprocess.PIPE) 220 | process2 = subprocess.Popen('grep Revision'.split(), stdin=process1.stdout, stdout=subprocess.PIPE) 221 | process3 = subprocess.Popen(['awk', '{print $3}'], stdin=process2.stdout) 222 | process1.stdout.close() 223 | process2.stdout.close() 224 | return process3.communicate() 225 | 226 | 227 | def get_sysinfo(): 228 | return { 229 | "os_name": get_os_name() 230 | } 231 | 232 | 233 | def get_command_output(command_array): 234 | return subprocess.check_output(command_array) 235 | 236 | 237 | def get_user_email_git(): 238 | if cmd_exists("git"): 239 | command = "git config user.email" 240 | output = get_command_output(command.split()) 241 | if output is not None and len(output) > 0: 242 | return output.strip() 243 | return None 244 | else: 245 | return None 246 | 247 | 248 | def email_is_valid(email): 249 | return True if re.match(email_regex, email) else False 250 | 251 | 252 | def is_valid_github_url(url): 253 | return True if re.match(github_url_regex, url) else False 254 | -------------------------------------------------------------------------------- /snipsmanager/utils/pip_installer.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ pip module installer. """ 3 | 4 | import os 5 | import pip 6 | 7 | from .os_helpers import execute_command, is_valid_github_url, read_file 8 | 9 | from .. import SNIPS_CACHE_DIR 10 | from .. import VENV_PATH, SHELL_COMMAND, PIP_BINARY 11 | 12 | class PipInstallerException(Exception): 13 | pass 14 | 15 | # pylint: disable=too-few-public-methods 16 | class PipInstaller: 17 | """ pip module installer. """ 18 | 19 | @staticmethod 20 | def install(url_or_pip, force_download=False): 21 | """ Install a Python module. 22 | 23 | :param url_or_pip: URL of the module, or pip ID. 24 | """ 25 | if is_valid_github_url(url_or_pip): 26 | PipInstaller.install_url(url_or_pip, force_download=force_download) 27 | else: 28 | PipInstaller.install_pip(url_or_pip, force_download=force_download) 29 | 30 | 31 | @staticmethod 32 | def install_pip(package_name, force_download=False): 33 | no_cache = "--no-cache" if force_download else "" 34 | PipInstaller.execute_pip_install("--upgrade --quiet {} {}".format(no_cache, package_name)) 35 | 36 | 37 | @staticmethod 38 | def install_url(url, force_download=False): 39 | if url.startswith("https://"): 40 | url = "git+" + url 41 | 42 | if not force_download and PipCache.is_installed(url): 43 | return 44 | 45 | params = ["--upgrade", "--quiet"] 46 | if force_download: 47 | params.append("--force-reinstall") 48 | PipInstaller.execute_pip_install("{} {}".format(" ".join(params), url)) 49 | PipCache.add(url) 50 | 51 | 52 | @staticmethod 53 | def execute_pip_install(arguments): 54 | is_venv_active = PipInstaller.activate_venv() 55 | (output, error) = execute_command("{} install {}".format(PIP_BINARY, arguments), silent=False) 56 | if is_venv_active: 57 | PipInstaller.deactivate_venv() 58 | if error is not None and error.strip() != '': 59 | raise PipInstallerException(error) 60 | 61 | @staticmethod 62 | def activate_venv(): 63 | if VENV_PATH is None: 64 | return False 65 | try: 66 | execute_command("{} {}/bin/activate".format(SHELL_COMMAND, VENV_PATH), silent=True) 67 | return True 68 | except Exception as e: 69 | return False 70 | 71 | @staticmethod 72 | def deactivate_venv(): 73 | try: 74 | execute_command("deactivate", silent=True) 75 | except: 76 | pass 77 | 78 | 79 | class PipCache: 80 | 81 | STORE_FILE = os.path.join(SNIPS_CACHE_DIR, "pip_cache") 82 | 83 | @staticmethod 84 | def is_installed(github_url): 85 | cache = read_file(PipCache.STORE_FILE) 86 | if cache is not None and cache.strip() != '': 87 | return cache.find(github_url) != -1 88 | return False 89 | 90 | @staticmethod 91 | def add(github_url): 92 | if PipCache.is_installed(github_url): 93 | return 94 | with open(PipCache.STORE_FILE, "a") as f: 95 | f.write(github_url + "\n") 96 | -------------------------------------------------------------------------------- /snipsmanager/utils/snips.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Utilities for managing the Snips SDK. """ 3 | 4 | import os 5 | import subprocess 6 | 7 | from .os_helpers import cmd_exists, is_raspi_os, ask_yes_no, execute_command 8 | 9 | try: 10 | from subprocess import DEVNULL 11 | except ImportError: 12 | DEVNULL = open(os.devnull, 'wb') 13 | 14 | # pylint: disable=too-few-public-methods 15 | 16 | 17 | SNIPS_INSTALL_COMMAND = "curl https://install.snips.ai -sSf" 18 | SNIPS_INSTALL_ASSISTANT_COMMAND = "snips-install-assistant" 19 | SNIPS_CONFIG_PATH="/usr/share/snips" 20 | 21 | 22 | class SnipsUnsupportedPlatform(Exception): 23 | """ Unsupported platform exception class. """ 24 | pass 25 | 26 | 27 | class SnipsNotFound(Exception): 28 | """ Snips command not found exception class. """ 29 | pass 30 | 31 | 32 | class SnipsRuntimeFailure(Exception): 33 | """ Snips runtime failure exception class. """ 34 | pass 35 | 36 | 37 | class SnipsInstallationFailure(Exception): 38 | """ Snips installation failure exception class. """ 39 | pass 40 | 41 | 42 | class Snips: 43 | """ Utilities for managing the Snips SDK. """ 44 | 45 | @staticmethod 46 | def is_installed(): 47 | """ Check if the Snips SDK is installed. """ 48 | return cmd_exists("snips") or cmd_exists("snips-asr") or cmd_exists("snips-hotword") 49 | 50 | @staticmethod 51 | def load_assistant(assistant_zip_path): 52 | """ Load an assistant file for the Snips SDK. 53 | 54 | :param assistant_zip_path: The path to the assistant.zip file. 55 | """ 56 | if not cmd_exists(SNIPS_INSTALL_ASSISTANT_COMMAND): 57 | execute_command("sudo rm -rf " + SNIPS_CONFIG_PATH + "/assistant") 58 | execute_command("sudo unzip " + assistant_zip_path + " -d " + SNIPS_CONFIG_PATH) 59 | return 60 | 61 | command = "{} {}".format(SNIPS_INSTALL_ASSISTANT_COMMAND, assistant_zip_path) 62 | process = subprocess.Popen(command.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 63 | output, error = process.communicate() 64 | return process.returncode 65 | -------------------------------------------------------------------------------- /snipsmanager/utils/speaker_setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from .os_helpers import is_raspi_os, execute_command 4 | from .. import ASOUNDCONF_DEST_PATH 5 | 6 | class SpeakerSetup: 7 | ASOUNDCONF_PATH = "../config/asound.conf/" 8 | SOUND_DRIVER_PATH = "../config/drivers/" 9 | 10 | @staticmethod 11 | def setup_asoundconf(speaker_id): 12 | if not is_raspi_os(): 13 | return 14 | elif speaker_id == 'adafruit-bonnet': 15 | SpeakerSetup._copy_asoundconf("asound.conf.speakerbonnet") 16 | 17 | 18 | @staticmethod 19 | def setup_driver(speaker_id): 20 | if not is_raspi_os(): 21 | return 22 | elif speaker_id == 'adafruit-bonnet': 23 | SpeakerSetup._install_driver("adafruit_bonnet.sh") 24 | 25 | @staticmethod 26 | def _copy_asoundconf(asoundconf_file): 27 | """ Copy asoundconf configuration to local path. 28 | 29 | :param asoundconf_file: the name of the asoundconf configuration, as 30 | present in the config folder. 31 | """ 32 | this_dir, this_filename = os.path.split(__file__) 33 | asoundconf_path = os.path.join(this_dir, SpeakerSetup.ASOUNDCONF_PATH, asoundconf_file) 34 | destination = os.path.expanduser(ASOUNDCONF_DEST_PATH) 35 | shutil.copy2(asoundconf_path, destination) 36 | 37 | 38 | @staticmethod 39 | def _install_driver(driver_file): 40 | if not is_raspi_os(): 41 | return 42 | this_dir, this_filename = os.path.split(__file__) 43 | driver_path = os.path.join(this_dir, SpeakerSetup.SOUND_DRIVER_PATH, driver_file) 44 | execute_command("sudo chmod a+x " + driver_path) 45 | execute_command(driver_path + " -y") 46 | -------------------------------------------------------------------------------- /snipsmanager/utils/systemd.py: -------------------------------------------------------------------------------- 1 | # -*-: coding utf-8 -*- 2 | """ Systemd utilities. """ 3 | 4 | import getpass 5 | import os 6 | import subprocess 7 | import time 8 | 9 | from .os_helpers import execute_command, ask_yes_no, which 10 | 11 | SNIPSMANAGER_SERVICE_NAME = "snipsmanager" 12 | 13 | 14 | class Systemd: 15 | """ Systemd utilities. """ 16 | 17 | @staticmethod 18 | def setup(use_default_values=None): 19 | if ask_yes_no("Would you like Snips to start on boot (using systemd)?", use_default_values) == False: 20 | return 21 | 22 | (username, snips_home_path, snipsmanager_path, 23 | snips_path) = Systemd.get_snipsmanager_params(use_default_values=use_default_values) 24 | Systemd.write_snipsmanager_file( 25 | username, snips_home_path, snipsmanager_path) 26 | Systemd.enable_service(username, SNIPSMANAGER_SERVICE_NAME) 27 | 28 | @staticmethod 29 | def get_snipsmanager_params(use_default_values=None): 30 | current_username = getpass.getuser() 31 | if use_default_values == True: 32 | username = current_username 33 | else: 34 | username = raw_input( 35 | "Run as user [default: {}]: ".format(current_username)) 36 | if username is None or username.strip() == "": 37 | username = current_username 38 | 39 | snips_home_path = os.getcwd() 40 | 41 | snipsmanager_path = which('snipsmanager') 42 | if snipsmanager_path is None or len(snipsmanager_path.strip()) == 0: 43 | if use_default_values != True: 44 | snipsmanager_path = raw_input("Path to the snipsmanager binary: ") 45 | 46 | snips_path = which('snips') 47 | if snips_path is None or len(snips_path.strip()) == 0: 48 | if use_default_values != True: 49 | snips_path = raw_input("Path to the snips binary: ") 50 | 51 | return (username, snips_home_path, snipsmanager_path, snips_path) 52 | 53 | @staticmethod 54 | def write_snipsmanager_file(username, snips_home_path, snipsmanager_path): 55 | contents = Systemd.get_template(SNIPSMANAGER_SERVICE_NAME) 56 | if contents is None: 57 | return 58 | contents = contents.replace("{{SNIPS_HOME_PATH}}", snips_home_path) \ 59 | .replace("{{SNIPSMANAGER_PATH}}", snipsmanager_path) 60 | Systemd.write_systemd_file( 61 | SNIPSMANAGER_SERVICE_NAME, username, contents) 62 | 63 | @staticmethod 64 | def get_template(service_name): 65 | this_dir, this_filename = os.path.split(__file__) 66 | template_filename = os.path.join( 67 | this_dir, "../config/systemd/{}.service".format(service_name)) 68 | with open(template_filename, 'r') as template_file: 69 | return template_file.read() 70 | return None 71 | 72 | @staticmethod 73 | def write_systemd_file(service_name, username, contents): 74 | if username is None: 75 | # Run as root 76 | output_filename = "/etc/systemd/system/{}.service".format( 77 | service_name) 78 | else: 79 | output_filename = "/etc/systemd/system/{}@{}.service".format( 80 | service_name, username) 81 | tmp_filename = "tmp_{}".format(service_name) 82 | os.system("sudo echo \"{}\" > {}".format(contents, tmp_filename)) 83 | os.system("sudo mv {} {}".format(tmp_filename, output_filename)) 84 | os.system("sudo chmod a+rwx {}".format(output_filename)) 85 | 86 | @staticmethod 87 | def enable_service(username, service_name): 88 | os.system("sudo systemctl --system daemon-reload") 89 | if username is None: 90 | # Run as root 91 | os.system("sudo systemctl enable {}".format(service_name)) 92 | else: 93 | os.system("sudo systemctl enable {}@{}".format(service_name, username)) 94 | -------------------------------------------------------------------------------- /snipsmanager/utils/wizard.py: -------------------------------------------------------------------------------- 1 | class Wizard(object): 2 | def __init__(self): 3 | self._questions = list() 4 | 5 | def __len__(self): 6 | return len(self._questions) 7 | 8 | def __getitem__(self, position): 9 | return self._questions[position] 10 | 11 | def add_question(self, text, description, input_function, input_validation, default_value=None): 12 | question = Question(text=text, description=description, input_function=input_function, 13 | input_validation=input_validation, default_value=default_value) 14 | self._questions.append(question) 15 | 16 | def run(self): 17 | return [question.answer() for question in self._questions] 18 | 19 | 20 | class Question(object): 21 | def __init__(self, text, description, input_function, input_validation, default_value=None): 22 | self.text = text 23 | self.description = description 24 | self.input_function = input_function 25 | self.input_validation = input_validation 26 | self.default_value = default_value 27 | 28 | def answer(self): 29 | if len(self.description) > 0: 30 | print self.description 31 | 32 | result = self.input_function(self.text) if not self.default_value else self.input_function(self.text, 33 | self.default_value) 34 | while (not self.input_validation(result)): 35 | result = self.input_function(self.text) if not self.default_value else self.input_function(self.text, 36 | self.default_val) 37 | return result 38 | -------------------------------------------------------------------------------- /stdeb.cfg: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | Depends: python-pip, libsdl-mixer1.2, libusb-1.0, python-pyaudio, libsdl1.2-dev, cython, cython3, libudev-dev, python-dev, libsdl-image1.2-dev, libsdl-mixer1.2-dev, libsdl-ttf2.0-dev, libsmpeg-dev, python-numpy, libportmidi-dev, libswscale-dev, libavformat-dev, libavcodec-dev, portaudio19-dev, nodejs, build-essential -------------------------------------------------------------------------------- /tests/auth/test_auth.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from snipsmanager.utils.assistant_downloader import AuthDownloader, AuthExceptionInvalidCredentials, AuthExceptionInvalidAssistantId 3 | 4 | class AuthTest(TestCase): 5 | def setUp(self): 6 | pass 7 | 8 | class AuthTest(TestCase): 9 | def test_input_wrong_email(self): 10 | with self.assertRaises(AuthExceptionInvalidCredentials): 11 | AuthDownloader("anthony", "password123", "proj_xxxxxxxxx") 12 | 13 | with self.assertRaises(AuthExceptionInvalidCredentials): 14 | AuthDownloader("anthony.test", "password123", "proj_xxxxxxxxx") 15 | 16 | with self.assertRaises(AuthExceptionInvalidCredentials): 17 | AuthDownloader("anthony.test@test", "password123", "proj_xxxxxxxxx") 18 | 19 | with self.assertRaises(AuthExceptionInvalidCredentials): 20 | AuthDownloader("anthony.test@provider.", "password123", "proj_xxxxxxxxx") 21 | 22 | def test_input_password(self): 23 | with self.assertRaises(AuthExceptionInvalidCredentials): 24 | AuthDownloader("anthony.test@provider.com", "", "proj_xxxxxxxxx") 25 | 26 | def test_input_assistantId(self): 27 | with self.assertRaises(AuthExceptionInvalidAssistantId): 28 | AuthDownloader("user.test@provider.com","password123", "proj_") 29 | 30 | -------------------------------------------------------------------------------- /tests/commands/test_install.py: -------------------------------------------------------------------------------- 1 | """Tests for the `snipsskills install` command.""" 2 | 3 | 4 | from subprocess import PIPE, Popen as popen 5 | from unittest import TestCase 6 | 7 | 8 | class TestHello(TestCase): 9 | 10 | def test_returns_multiple_lines(self): 11 | self.assertTrue(True) 12 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | """Tests for our main snipsmanager CLI module.""" 2 | 3 | 4 | from subprocess import PIPE, Popen as popen 5 | from unittest import TestCase 6 | 7 | # from snipsmanager import __version__ as VERSION 8 | 9 | 10 | # class TestHelp(TestCase): 11 | 12 | # def test_returns_usage_information(self): 13 | # output = popen(['snipsmanager', '-h'], stdout=PIPE).communicate()[0] 14 | # self.assertTrue('Usage:' in output) 15 | 16 | # output = popen(['snipsmanager', '--help'], stdout=PIPE).communicate()[0] 17 | # self.assertTrue('Usage:' in output) 18 | 19 | 20 | # class TestVersion(TestCase): 21 | # def test_returns_version_information(self): 22 | # output = popen(['snipsmanager', '--version'], stdout=PIPE).communicate()[0] 23 | # self.assertEqual(output.strip(), VERSION) 24 | -------------------------------------------------------------------------------- /tests/utils/test_wizard.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from snipsmanager.utils.wizard import Wizard 4 | from snipsmanager.utils.os_helpers import ask_yes_no 5 | 6 | 7 | class BaseTest(TestCase): 8 | def setUp(self): 9 | pass 10 | 11 | 12 | class WizardTest(BaseTest): 13 | def setUp(self): 14 | self.wizard = Wizard() 15 | self.wizard.add_question(text="dummy1", 16 | description="dummy1", 17 | input_function=lambda x: x, 18 | input_validation=lambda x: True) 19 | self.wizard.add_question(text="dummy2", 20 | description="dummy2", 21 | input_function=lambda x: x, 22 | input_validation=lambda x: True) 23 | self.wizard.add_question(text="dummy3", 24 | description="dummy3", 25 | input_function=lambda x: x, 26 | input_validation=lambda x: True) 27 | 28 | def test_number_of_questions(self): 29 | self.assertEqual(len(self.wizard), 3) 30 | 31 | def test_number_of_results(self): 32 | self.assertEqual(len([question.answer() for question in self.wizard]), 3) 33 | 34 | --------------------------------------------------------------------------------