diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index dbc623d4..8a95c3ba 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -9,6 +9,7 @@ In order to help troubleshooting, be sure to include the following information: **Output of the [audio debugging script](https://github.com/alexa-pi/AlexaPi/wiki/Audio-setup-&-debugging#audio-debugging-script).** **Your OS (including version) where you are running AlexaPi:** +_Note: Raspbian older than Stretch is not supported!_ ``` ``` @@ -17,7 +18,7 @@ In order to help troubleshooting, be sure to include the following information: ``` -**Python release (`python2 --version`):** +**Python release (`python3 --version`):** ``` ``` diff --git a/.gitignore b/.gitignore index db00e7ba..66e27ed8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *__pycache__* *.pyc +.venv + src/config.yaml .idea diff --git a/.travis.yml b/.travis.yml index 6e63091d..d6017beb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,29 +3,26 @@ language: python cache: pip python: - - "2.7" - - "3.4" - "3.5" -# pylint doesn't work on 3.6 yet -# - "3.6" + - "3.6" + - "3.7" -dist: trusty -# On the container-based infrastructure, trusty-backports isn't available, hence this silly thing -sudo: required +dist: xenial -# It installs old shellcheck from trusty-backports -# Installing from Debian Sid or Ubuntu Zesty doesn't work -# Some discussion: https://github.com/koalaman/shellcheck/issues/785 before_install: - sudo apt-get update - - sudo apt-get install -y wget git swig portaudio19-dev libpulse-dev vlc-nox shellcheck + - sudo apt-get install -y wget git portaudio19-dev libpulse-dev vlc-nox shellcheck sox libatlas-base-dev + - sudo wget http://mirrors.kernel.org/ubuntu/pool/universe/s/swig/swig3.0_3.0.10-1.2_amd64.deb + - sudo dpkg -i swig3.0_3.0.10-1.2_amd64.deb + - sudo ln -s /usr/bin/swig3.0 /usr/local/bin/swig + - pip install pipenv install: - - pip install -r src/requirements.txt - - pip install -r src/dev-requirements.txt - - yes | pip install git+https://github.com/duxingkei33/orangepi_PC_gpio_pyH3.git -v + - pipenv install --dev --deploy + - pipenv install git+https://github.com/Kitt-AI/snowboy.git#egg=snowboy || true +# - yes | pip install git+https://github.com/duxingkei33/orangepi_PC_gpio_pyH3.git#egg=pyA20 -v script: - - pylint --rcfile=pylintrc --ignore=tunein.py src/auth_web.py src/main.py src/alexapi - - python -c "import yaml; yaml.load(open('src/config.template.yaml'))" - - cd src/scripts && shellcheck -e 2164 ./inc/*.sh ./inc/os/*.sh ./inc/device/*.sh ./*.sh + - pipenv run lint + - pipenv run yamlcheck + - pipenv run shlint diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cbf0d80..9723ffe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ## [Unreleased] +## [1.7] - 2018-12-22 + +### Added +- Support for snowboy - the awesome trigger word detector +- Debug option to `auth_web.py`. + +### Changed +- Python 3 is default. +- Uses `pip` version from the repo on Debian systems instead of the dirty uninstall & install via `easy_install`. + +### Removed +- Support for Python < 3.5 +- Support for Debian/Raspbian older than Stretch - Jessie is not supported anymore! + +### Fixed +- Weird bugs with dependencies versions, as we now use locked deps. +- Broken Raspbian installs due to some Python version mishap. + ## [1.6] - 2017-10-08 ### Added @@ -147,7 +165,8 @@ This is mainly a test of doing bugfix releases. @sammachin created the project in January 2016 and made significant changes that lead to this version. -[Unreleased]: https://github.com/alexa-pi/AlexaPi/compare/v1.6...HEAD +[Unreleased]: https://github.com/alexa-pi/AlexaPi/compare/v1.7...HEAD +[1.7]: https://github.com/alexa-pi/AlexaPi/compare/v1.6...v1.7 [1.6]: https://github.com/alexa-pi/AlexaPi/compare/v1.5.1...v1.6 [1.5.1]: https://github.com/alexa-pi/AlexaPi/compare/v1.5...v1.5.1 [1.5]: https://github.com/alexa-pi/AlexaPi/compare/v1.4...v1.5 diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..a858400d --- /dev/null +++ b/Pipfile @@ -0,0 +1,29 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pylint = "*" +pyserial = "*" +websocket-client = "*" +fakerpigpio = "==0.3a0" +chip-io = {git = "https://github.com/xtacocorex/CHIP_IO.git"} + +[packages] +requests = "*" +python-vlc = "*" +webrtcvad = "*" +pocketsphinx = "*" +coloredlogs = "*" +CherryPy = "*" +PyAudio = "*" +PyYAML = "*" + +[requires] +#python_version = "3.7" + +[scripts] +lint = "pylint --rcfile=pylintrc --ignore=tunein.py src/auth_web.py src/main.py src/alexapi" +yamlcheck = "python -c \"import yaml; yaml.load(open('src/config.template.yaml'))\"" +shlint = "pipenv shell --anyway \"cd src/scripts && shellcheck -e 2164 -e 1117 ./inc/*.sh ./inc/os/*.sh ./inc/device/*.sh ./*.sh\" && exit" \ No newline at end of file diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000..3b150279 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,313 @@ +{ + "_meta": { + "hash": { + "sha256": "b128485c281949dd0a79aafcf5133f602ea8ed1f1f12ec4b35b68b1056835420" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "backports.functools-lru-cache": { + "hashes": [ + "sha256:9d98697f088eb1b0fa451391f91afb5e3ebde16bbdb272819fd091151fda4f1a", + "sha256:f0b0e4eba956de51238e17573b7087e852dfe9854afd2e9c873f73fc0ca0a6dd" + ], + "version": "==1.5" + }, + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "cheroot": { + "hashes": [ + "sha256:ba2b9b0f59fcdcd920d51f61c19c4a8d65f8469741e634c04eaf6feca5b76837", + "sha256:f1bb443f266898bc516de78a98e601e0681770a2f2310c2335648c4985e35184" + ], + "version": "==6.5.2" + }, + "cherrypy": { + "hashes": [ + "sha256:4dd2f59b5af93bd9ca85f1ed0bb8295cd0f5a8ee2b84d476374d4e070aa5c615", + "sha256:626e305bca3c5d56a16e5f7d64bc8a4e25d26c41be1779f585fad2608edbc4c8" + ], + "index": "pypi", + "version": "==18.1.0" + }, + "coloredlogs": { + "hashes": [ + "sha256:34fad2e342d5a559c31b6c889e8d14f97cb62c47d9a2ae7b5ed14ea10a79eff8", + "sha256:b869a2dda3fa88154b9dd850e27828d8755bfab5a838a1c97fbc850c6e377c36" + ], + "index": "pypi", + "version": "==10.0" + }, + "humanfriendly": { + "hashes": [ + "sha256:1d3a1c157602801c62dfdb321760229df2e0d4f14412a0f41b13ad3f930a936a", + "sha256:42d0aa829f59c710db20ec42eed24a8b7a27688d477da61b5aebd604d0bb2402" + ], + "version": "==4.17" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "jaraco.functools": { + "hashes": [ + "sha256:0cc09711acc0c716a2011a6924c60b2092ac07261aa1822e4277a0f0c5bf0548", + "sha256:bad775f06e58bb8de5563bc2a8bf704168919e6779d6e849b1ca58b443e97f3b" + ], + "version": "==1.20" + }, + "more-itertools": { + "hashes": [ + "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", + "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", + "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" + ], + "version": "==4.3.0" + }, + "pocketsphinx": { + "hashes": [ + "sha256:1c2a8cc7f2032f9e8214f5e4e864511fe37bfd27f2045fbf8aed9c54569dc881", + "sha256:1f3eaca1b49d579e89e909bb101ad033493a1c9dddd8c72afd6b755c70365d56", + "sha256:34d290745c7dbe6fa2cac9815b5c19d10f393e528ecd70e779c21ebc448f9b63", + "sha256:3f438f9f5729a7629c122784697884cff5f2a2e022aef270a4081985d15eae88", + "sha256:41d2107b74597cf8444f9e36b91363d60097b4fa88a9c750760a85209deef439", + "sha256:667152b4139ba46de172fa627404fe284b2ecd5d6bfcfe905b068801324ba7fb", + "sha256:71e7cf04433615e21ad3c28a2cc2daa43e96e570f2611f142eb3468f4f7789a6", + "sha256:72286c3ecaa6641d426ca80bd4d1b3241c26b1703af419429ef779719e007443", + "sha256:855c6761d008cdb4fc2d9aded5c1a0f163ce901f8b64075d36a88ae7814af755", + "sha256:8ca14694d7cc111796e63c13c61420553a43a29c63c386c975bfc213d8da3e55", + "sha256:9b477fcf8829f53ab470fdcf3fa77db00d71eee321353d10b306f0e5b95e8591", + "sha256:bfbbeddb20196abbb04b8f7b5dc03628c7df0dc87e760326d76d37b27fb9d6f9", + "sha256:d20b497369bd108dd135b3d4eed1d3edd2c8b51b1b696e67600adbfff2fe3316", + "sha256:e2a4f22cddec68e75e796068ae82095dd8dba1187aa3a8abe305bdc6c565e8b4", + "sha256:f0fdfe162bc99590e8666251a55d6311ac4cb137482a871f93d38b1834251253" + ], + "index": "pypi", + "version": "==0.1.15" + }, + "portend": { + "hashes": [ + "sha256:b7ce7d35ea262415297cbfea86226513e77b9ee5f631d3baa11992d663963719", + "sha256:f5c99a1aa1655733736bb0283fee6a1e115e18db500332bec8e24c43f320d8e8" + ], + "version": "==2.3" + }, + "pyaudio": { + "hashes": [ + "sha256:0d92f6a294565260a282f7c9a0b0d309fc8cc988b5ee5b50645634ab9e2da7f7", + "sha256:259bb9c1363be895b4f9a97e320a6017dd06bc540728c1a04eb4a7b6fe75035b", + "sha256:2a19bdb8ec1445b4f3e4b7b109e0e4cec1fd1f1ce588592aeb6db0b58d4fb3b0", + "sha256:51b558d1b28c68437b53218279110db44f69f3f5dd3d81859f569a4a96962bdc", + "sha256:589bfad2c615dd4b5d3757e763019c42ab82f06fba5cae64ec02fd7f5ae407ed", + "sha256:8f89075b4844ea94dde0c951c2937581c989fabd4df09bfd3f075035f50955df", + "sha256:93bfde30e0b64e63a46f2fd77e85c41fd51182a4a3413d9edfaf9ffaa26efb74", + "sha256:cf1543ba50bd44ac0d0ab5c035bb9c3127eb76047ff12235149d9adf86f532b6", + "sha256:f78d543a98b730e64621ebf7f3e2868a79ade0a373882ef51c0293455ffa8e6e" + ], + "index": "pypi", + "version": "==0.2.11" + }, + "python-vlc": { + "hashes": [ + "sha256:b6cb026dcd65a367b95a37d37eb461e7ae9be261617884679e3e7cadae891068" + ], + "index": "pypi", + "version": "==3.0.4106" + }, + "pytz": { + "hashes": [ + "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca", + "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6" + ], + "version": "==2018.7" + }, + "pyyaml": { + "hashes": [ + "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", + "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", + "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", + "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", + "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", + "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", + "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", + "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", + "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", + "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + ], + "index": "pypi", + "version": "==3.13" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "tempora": { + "hashes": [ + "sha256:4951da790bd369f718dbe2287adbdc289dc2575a09278e77fad6131bcfe93097", + "sha256:f8abbbd486eca3340bd3d242417b203c861d4e113ef778cd5fb9535b2b32ae54" + ], + "version": "==1.14" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + }, + "webrtcvad": { + "hashes": [ + "sha256:f1bed2fb25b63fb7b1a55d64090c993c9c9167b28485ae0bcdd81cf6ede96aea" + ], + "index": "pypi", + "version": "==2.0.10" + }, + "zc.lockfile": { + "hashes": [ + "sha256:95a8e3846937ab2991b61703d6e0251d5abb9604e18412e2714e1b90db173253" + ], + "version": "==1.4" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "chip-io": { + "git": "https://github.com/xtacocorex/CHIP_IO.git", + "ref": "84df4376f83bb4de2b4e2f77447cb75704ebdaf4" + }, + "fakerpigpio": { + "hashes": [ + "sha256:5cd9d716de2e7e906349597ff2dae88fe38314ecbc2554eee5d5ca88b8f30bb7" + ], + "index": "pypi", + "version": "==0.3a0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "pyserial": { + "hashes": [ + "sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627", + "sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8" + ], + "index": "pypi", + "version": "==3.4" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "websocket-client": { + "hashes": [ + "sha256:8c8bf2d4f800c3ed952df206b18c28f7070d9e3dcbd6ca6291127574f57ee786", + "sha256:e51562c91ddb8148e791f0155fdb01325d99bb52c4cdbb291aee7a3563fd0849" + ], + "index": "pypi", + "version": "==0.54.0" + }, + "wrapt": { + "hashes": [ + "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" + ], + "version": "==1.10.11" + } + } +} diff --git a/pylintrc b/pylintrc index 40b2e87f..5df46dd8 100644 --- a/pylintrc +++ b/pylintrc @@ -158,7 +158,7 @@ notes=XXX [SIMILARITIES] # Minimum lines number of a similarity. -min-similarity-lines=5 +min-similarity-lines=6 # Ignore comments when computing similarities. ignore-comments=yes diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..9ec97adb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,23 @@ +-i https://pypi.org/simple +backports.functools-lru-cache==1.5 +certifi==2018.11.29 +chardet==3.0.4 +cheroot==6.5.2 +cherrypy==18.1.0 +coloredlogs==10.0 +humanfriendly==4.17 +idna==2.8 +jaraco.functools==1.20 +more-itertools==4.3.0 +pocketsphinx==0.1.15 +portend==2.3 +pyaudio==0.2.11 +python-vlc==3.0.4106 +pytz==2018.7 +pyyaml==3.13 +requests==2.21.0 +six==1.12.0 +tempora==1.14 +urllib3==1.24.1 +webrtcvad==2.0.10 +zc.lockfile==1.4 diff --git a/src/alexapi/capture.py b/src/alexapi/capture.py index 7d851a36..93e9e6c9 100644 --- a/src/alexapi/capture.py +++ b/src/alexapi/capture.py @@ -15,7 +15,7 @@ logger = logging.getLogger(__name__) -class DeviceInfo(object): +class DeviceInfo: _pa = None @@ -41,7 +41,7 @@ def __del__(self): self._pa.terminate() -class Capture(object): +class Capture: MAX_RECORDING_LENGTH = 8 diff --git a/src/alexapi/constants.py b/src/alexapi/constants.py index 52a484cc..ed0c70fb 100644 --- a/src/alexapi/constants.py +++ b/src/alexapi/constants.py @@ -1,6 +1,6 @@ -class RequestType(object): +class RequestType: STARTED = 'STARTED' INTERRUPTED = 'INTERRUPTED' FINISHED = 'FINISHED' @@ -11,7 +11,7 @@ def __init__(self): -class PlayerActivity(object): +class PlayerActivity: PLAYING = 'PLAYING' PAUSED = 'PAUSED' IDLE = 'IDLE' diff --git a/src/alexapi/device_platforms/magicmirrorplatform.py b/src/alexapi/device_platforms/magicmirrorplatform.py index 19870430..aaf7d58d 100644 --- a/src/alexapi/device_platforms/magicmirrorplatform.py +++ b/src/alexapi/device_platforms/magicmirrorplatform.py @@ -6,12 +6,8 @@ except ImportError: import http.server as BaseHTTPServer -try: - from urllib.request import urlopen, URLError - import urllib.urlparse as urlparse -except ImportError: - from urllib2 import urlopen, URLError - import urlparse +from urllib.request import urlopen, URLError +import urllib.parse as urlparse from .baseplatform import BasePlatform @@ -120,7 +116,7 @@ def mm_heartbeat(self): logger.error("URLError: %s", err.reason) return - logger.debug("Response: " + response) + logger.debug("Response: %s", response) def http_callback(self, query_dict): if (query_dict['action'][0] == "requestrecord"): @@ -129,8 +125,8 @@ def http_callback(self, query_dict): self._trigger_callback() return True - else: - return False + + return False def cleanup(self): logger.debug("Cleaning up Magic Mirror platform") diff --git a/src/alexapi/device_platforms/orangepiplatform.py b/src/alexapi/device_platforms/orangepiplatform.py index 269698c1..eef41a0b 100644 --- a/src/alexapi/device_platforms/orangepiplatform.py +++ b/src/alexapi/device_platforms/orangepiplatform.py @@ -1,7 +1,7 @@ import time import threading -from pyA20.gpio import gpio as GPIO +from pyA20.gpio import gpio as GPIO # pylint: disable=import-error from .rpilikeplatform import RPiLikePlatform diff --git a/src/alexapi/playback_handlers/basehandler.py b/src/alexapi/playback_handlers/basehandler.py index 0ab49688..cb4263f9 100644 --- a/src/alexapi/playback_handlers/basehandler.py +++ b/src/alexapi/playback_handlers/basehandler.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -class PlaybackAudioType(object): +class PlaybackAudioType: SPEECH = 'speech' MEDIA = 'media' @@ -18,7 +18,7 @@ def __init__(self): pass -class PlaybackItem(object): +class PlaybackItem: def __init__(self, url, offset, audio_type, stream_id): self.url = url self.offset = offset @@ -26,7 +26,7 @@ def __init__(self, url, offset, audio_type, stream_id): self.stream_id = stream_id -class PlaybackLock(object): +class PlaybackLock: def __init__(self): # This has inverted logic self.play_lock = threading.Event() @@ -44,7 +44,7 @@ def release(self): self.is_playing = False -class BaseHandler(object): +class BaseHandler: __metaclass__ = ABCMeta def __init__(self, config, callback_report): diff --git a/src/alexapi/triggers/__init__.py b/src/alexapi/triggers/__init__.py index 5e09a309..d701e473 100644 --- a/src/alexapi/triggers/__init__.py +++ b/src/alexapi/triggers/__init__.py @@ -38,12 +38,19 @@ def disable(type_filter=None): trigger.disable() -class TYPES(object): +def cleanup(type_filter=None): + for name in triggers: + trigger = triggers[name] + if (not type_filter) or (trigger.type == type_filter): + trigger.cleanup() + + +class TYPES: OTHER = 0 VOICE = 1 -class EVENT_TYPES(object): # pylint: disable=invalid-name +class EVENT_TYPES: # pylint: disable=invalid-name ONESHOT_VAD = 1 CONTINUOUS = 2 CONTINUOUS_VAD = 3 diff --git a/src/alexapi/triggers/basetrigger.py b/src/alexapi/triggers/basetrigger.py index aeaa49a5..ba707ac0 100644 --- a/src/alexapi/triggers/basetrigger.py +++ b/src/alexapi/triggers/basetrigger.py @@ -4,7 +4,7 @@ class BaseTrigger: __metaclass__ = ABCMeta - name = None + name = '' type = None event_type = None voice_confirm = None @@ -13,15 +13,18 @@ class BaseTrigger: _config = None _tconfig = None - def __init__(self, config, trigger_callback, trigger_name): - - self.name = trigger_name + def __init__(self, config, trigger_callback): self._config = config - self._tconfig = config['triggers'][trigger_name] + self._tconfig = config['triggers'][self.name] self._trigger_callback = trigger_callback self.voice_confirm = self._tconfig['voice_confirm'] + self.validate_config() + + def validate_config(self): + pass + @abstractmethod def setup(self): pass @@ -37,3 +40,6 @@ def enable(self): @abstractmethod def disable(self): pass + + def cleanup(self): + pass diff --git a/src/alexapi/triggers/platformtrigger.py b/src/alexapi/triggers/platformtrigger.py index 9a4bb700..270ce4f1 100644 --- a/src/alexapi/triggers/platformtrigger.py +++ b/src/alexapi/triggers/platformtrigger.py @@ -12,12 +12,13 @@ class PlatformTrigger(BaseTrigger): type = triggers.TYPES.OTHER + name = 'platform' _platform_continuous_callback = None def __init__(self, config, trigger_callback): - super(PlatformTrigger, self).__init__(config, trigger_callback, 'platform') + super(PlatformTrigger, self).__init__(config, trigger_callback) event_types = { 'oneshot-vad': triggers.EVENT_TYPES.ONESHOT_VAD, @@ -69,7 +70,7 @@ def long_press(self): pass # play_audio(self._pconfig['long_press']['audio_file'].replace('{resources_path}', resources_path)) - logger.info("-- " + str(self._tconfig['long_press']['duration']) + " second button press detected. Running specified command.") + logger.info("-- %s second button press detected. Running specified command.", str(self._tconfig['long_press']['duration'])) os.system(self._tconfig['long_press']['command']) break diff --git a/src/alexapi/triggers/pocketsphinxtrigger.py b/src/alexapi/triggers/pocketsphinxtrigger.py index 8ccd2c83..83d88098 100644 --- a/src/alexapi/triggers/pocketsphinxtrigger.py +++ b/src/alexapi/triggers/pocketsphinxtrigger.py @@ -6,15 +6,14 @@ from pocketsphinx import get_model_path from pocketsphinx.pocketsphinx import Decoder -import alexapi.triggers as triggers -from .basetrigger import BaseTrigger +from .voicetrigger import VoiceTrigger logger = logging.getLogger(__name__) -class PocketsphinxTrigger(BaseTrigger): +class PocketsphinxTrigger(VoiceTrigger): - type = triggers.TYPES.VOICE + name = 'pocketsphinx' AUDIO_CHUNK_SIZE = 1024 AUDIO_RATE = 16000 @@ -22,7 +21,7 @@ class PocketsphinxTrigger(BaseTrigger): _capture = None def __init__(self, config, trigger_callback, capture): - super(PocketsphinxTrigger, self).__init__(config, trigger_callback, 'pocketsphinx') + super(PocketsphinxTrigger, self).__init__(config, trigger_callback) self._capture = capture @@ -52,12 +51,7 @@ def setup(self): ps_config.set_string('-logfn', null_path) # Process audio chunk by chunk. On keyword detected perform action and restart search - self._decoder = Decoder(ps_config) - - def run(self): - thread = threading.Thread(target=self.thread, args=()) - thread.setDaemon(True) - thread.start() + self._detector = Decoder(ps_config) def thread(self): while True: @@ -65,7 +59,7 @@ def thread(self): self._capture.handle_init(self.AUDIO_RATE, self.AUDIO_CHUNK_SIZE) - self._decoder.start_utt() + self._detector.start_utt() triggered = False while not triggered: @@ -77,23 +71,15 @@ def thread(self): data = self._capture.handle_read() # Detect if keyword/trigger word was said - self._decoder.process_raw(data, False, False) + self._detector.process_raw(data, False, False) - triggered = self._decoder.hyp() is not None + triggered = self._detector.hyp() is not None self._capture.handle_release() - self._decoder.end_utt() + self._detector.end_utt() self._disabled_sync_lock.set() if triggered: self._trigger_callback(self) - - def enable(self): - self._enabled_lock.set() - self._disabled_sync_lock.clear() - - def disable(self): - self._enabled_lock.clear() - self._disabled_sync_lock.wait() diff --git a/src/alexapi/triggers/snowboytrigger.py b/src/alexapi/triggers/snowboytrigger.py new file mode 100644 index 00000000..678c3eca --- /dev/null +++ b/src/alexapi/triggers/snowboytrigger.py @@ -0,0 +1,154 @@ +import os +import logging +import time +import collections +import site + +import pyaudio +from snowboy import snowboydetect # pylint: disable=import-error + +from .voicetrigger import VoiceTrigger +from ..capture import DeviceInfo +from ..exceptions import ConfigurationException + +SNOWBOY_FOLDER = '' +try: + for packages_path in site.getsitepackages(): # pylint: disable=no-member + path_candidate = os.path.join(packages_path, 'snowboy') + if os.path.exists(path_candidate): + SNOWBOY_FOLDER = path_candidate +except AttributeError: + SNOWBOY_FOLDER = os.path.dirname(snowboydetect.__file__) + +logger = logging.getLogger(__name__) + + +# Copied from snowboy +class RingBuffer: + """Ring buffer to hold audio from PortAudio""" + def __init__(self, size=4096): + self._buf = collections.deque(maxlen=size) + + def extend(self, data): + """Adds data to the end of buffer""" + self._buf.extend(data) + + def get(self): + """Retrieves data from the beginning of buffer and clears it""" + tmp = bytes(bytearray(self._buf)) + self._buf.clear() + return tmp + + +class SnowboyTrigger(VoiceTrigger): + + name = 'snowboy' + + _sleep_time = 0.03 + _ring_buffer = None + + _pa = None + _device_info = None + + def __init__(self, config, trigger_callback, capture): # pylint: disable=unused-argument + super(SnowboyTrigger, self).__init__(config, trigger_callback) + + self._model = self._tconfig['model'].replace('{distribution}', os.path.join(SNOWBOY_FOLDER, 'resources')) + self._sensitivity = self._tconfig['sensitivity'] + + self._device_info = DeviceInfo() + + def validate_config(self): + + model_path = self._tconfig['model'].replace('{distribution}', os.path.join(SNOWBOY_FOLDER, 'resources')) + if not os.path.isfile(model_path): + raise ConfigurationException("Invalid snowboy model path: '" + model_path + "'") + + def setup(self): + # """ + # :param decoder_model: decoder model file path, a string or a list of strings + # :param resource: resource file path. + # :param sensitivity: decoder sensitivity, a float of a list of floats. + # The bigger the value, the more senstive the + # decoder. If an empty list is provided, then the + # default sensitivity in the model will be used. + # :param audio_gain: multiply input volume by this factor. + # """ + + audio_gain = 1 + + tm = type(self._model) + ts = type(self._sensitivity) + if tm is not list: + self._model = [self._model] + if ts is not list: + self._sensitivity = [self._sensitivity] + model_str = ",".join(self._model) + + resource_filename = os.path.join(SNOWBOY_FOLDER, "resources/common.res") + self._detector = snowboydetect.SnowboyDetect(resource_filename.encode(), model_str.encode()) + self._detector.SetAudioGain(audio_gain) + num_hotwords = self._detector.NumHotwords() + + if len(self._model) > 1 and len(self._sensitivity) == 1: + self._sensitivity = self._sensitivity * num_hotwords + + if self._sensitivity: + assert num_hotwords == len(self._sensitivity), \ + "number of hotwords in self._model (%d) and sensitivity " \ + "(%d) does not match" % (num_hotwords, len(self._sensitivity)) + + sensitivity_str = ",".join([str(t) for t in self._sensitivity]) + self._detector.SetSensitivity(sensitivity_str.encode()) + + self._ring_buffer = RingBuffer(self._detector.NumChannels() * self._detector.SampleRate() * 5) + + self._pa = pyaudio.PyAudio() + + def _audio_callback(self, in_data, frame_count, time_info, status): # pylint: disable=unused-argument + self._ring_buffer.extend(in_data) + play_data = chr(0) * len(in_data) + + return play_data, pyaudio.paContinue + + def thread(self): + + while True: + self._enabled_lock.wait() + + stream_in = self._pa.open( + input=True, + input_device_index=self._device_info.get_device_index(self._config['sound']['input_device']), + format=self._pa.get_format_from_width(self._detector.BitsPerSample() / 8), + channels=self._detector.NumChannels(), + rate=self._detector.SampleRate(), + frames_per_buffer=2048, + stream_callback=self._audio_callback) + + triggered = False + while not triggered: + if not self._enabled_lock.isSet(): + break + + data = self._ring_buffer.get() + if not data: + time.sleep(self._sleep_time) + continue + + ans = self._detector.RunDetection(data) + if ans == -1: + logger.warning("Error initializing streams or reading audio data") + elif ans > 0: + triggered = True + + stream_in.stop_stream() + stream_in.close() + + self._disabled_sync_lock.set() + + if triggered: + self._trigger_callback(self) + + def cleanup(self): + self.disable() + self._pa.terminate() diff --git a/src/alexapi/triggers/voicetrigger.py b/src/alexapi/triggers/voicetrigger.py new file mode 100644 index 00000000..a0166694 --- /dev/null +++ b/src/alexapi/triggers/voicetrigger.py @@ -0,0 +1,40 @@ +import threading +from abc import ABCMeta, abstractmethod + +import alexapi.triggers as triggers +from .basetrigger import BaseTrigger + + +class VoiceTrigger(BaseTrigger): + __metaclass__ = ABCMeta + + type = triggers.TYPES.VOICE + + _detector = None + + _enabled_lock = None + _disabled_sync_lock = None + + def __init__(self, config, trigger_callback): + + super(VoiceTrigger, self).__init__(config, trigger_callback) + + self._enabled_lock = threading.Event() + self._disabled_sync_lock = threading.Event() + + @abstractmethod + def thread(self): + pass + + def run(self): + thread = threading.Thread(target=self.thread, args=()) + thread.setDaemon(True) + thread.start() + + def enable(self): + self._enabled_lock.set() + self._disabled_sync_lock.clear() + + def disable(self): + self._enabled_lock.clear() + self._disabled_sync_lock.wait() diff --git a/src/alexapi/tunein.py b/src/alexapi/tunein.py index 6cf03116..b4c02f43 100644 --- a/src/alexapi/tunein.py +++ b/src/alexapi/tunein.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - try: import configparser except ImportError: @@ -9,10 +7,7 @@ import re import time -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse +from urllib.parse import urlparse from collections import OrderedDict from contextlib import closing diff --git a/src/auth_web.py b/src/auth_web.py index dd0398dd..01233d5b 100755 --- a/src/auth_web.py +++ b/src/auth_web.py @@ -1,28 +1,38 @@ #! /usr/bin/env python -from __future__ import print_function import os import json import socket import uuid import hashlib +import optparse +import logging +from urllib.parse import quote import yaml import cherrypy import requests -try: - from urllib.parse import quote -except ImportError: - from urllib import quote - import alexapi.config with open(alexapi.config.filename, 'r') as stream: config = yaml.load(stream) +parser = optparse.OptionParser() +parser.add_option('-d', '--debug', + dest="debug", + action="store_true", + default=False, + help="display debug messages") + +cmdopts, cmdargs = parser.parse_args() +debug = cmdopts.debug + +if debug: + logging.basicConfig(level=logging.DEBUG) + -class Start(object): +class Start: def index(self): sd = json.dumps({ diff --git a/src/config.template.yaml b/src/config.template.yaml index 51216394..d9a6be05 100644 --- a/src/config.template.yaml +++ b/src/config.template.yaml @@ -76,6 +76,21 @@ triggers: phrase: "alexa" threshold: 1e-10 + snowboy: + enabled: false + voice_confirm: true + + # Use your own model or a file from the default snowboy distribution: + # + # {distribution}/alexa.umdl + # {distribution}/snowboy.umdl + # {distribution}/alexa/alexa_02092017.umdl + # {distribution}/alexa/alexa-avs-sample-app/alexa.umdl + + model: "{distribution}/alexa/alexa_02092017.umdl" + sensitivity: 0.5 + + # Commands to run before and after an interaction begins. Leave empty to disable. event_commands: diff --git a/src/debug.bat b/src/debug.bat index b244291a..b48d11b9 100644 --- a/src/debug.bat +++ b/src/debug.bat @@ -2,6 +2,6 @@ echo off title AlexaPi for Windows cls -python main.py -d +python3 main.py -d pause \ No newline at end of file diff --git a/src/dev-requirements.txt b/src/dev-requirements.txt deleted file mode 100644 index ac0a3a69..00000000 --- a/src/dev-requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -pylint -fakeRPiGPIO --e git+https://github.com/xtacocorex/CHIP_IO.git#egg=CHIP_IO -websocket-client -pyserial \ No newline at end of file diff --git a/src/main.py b/src/main.py index 9990255a..a7a054af 100755 --- a/src/main.py +++ b/src/main.py @@ -14,7 +14,6 @@ import email import subprocess import hashlib -from future.builtins import bytes import yaml import requests @@ -110,7 +109,7 @@ platform = cl(config) -class Player(object): +class Player: config = None platform = None @@ -221,7 +220,7 @@ def internet_on(): return False -class Token(object): +class Token: _token = '' _timestamp = None @@ -267,7 +266,7 @@ def renew(self): logger.info("AVS token: Obtained successfully") except requests.exceptions.RequestException as exp: - logger.critical("AVS token: Failed to obtain a token: " + str(exp)) + logger.critical("AVS token: Failed to obtain a token: %s", str(exp)) # from https://github.com/respeaker/Alexa/blob/master/alexa.py @@ -543,6 +542,7 @@ def trigger_process(trigger): def cleanup(signal, frame): # pylint: disable=redefined-outer-name,unused-argument triggers.disable() + triggers.cleanup() capture.cleanup() pHandler.cleanup() platform.cleanup() @@ -561,15 +561,14 @@ def cleanup(signal, frame): # pylint: disable=redefined-outer-name,unused-argu try: capture = alexapi.capture.Capture(config, tmp_path) + capture.setup(platform.indicate_recording) + + triggers.init(config, trigger_callback, capture) + triggers.setup() except ConfigurationException as exp: logger.critical(exp) sys.exit(1) - capture.setup(platform.indicate_recording) - - triggers.init(config, trigger_callback, capture) - triggers.setup() - pHandler.setup() platform.setup() diff --git a/src/requirements.txt b/src/requirements.txt deleted file mode 100644 index 115410fd..00000000 --- a/src/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -requests>=2.13.0 -CherryPy>=10.1.1 -python-vlc -pyaudio>=0.2.10 -webrtcvad>=2.0.10 -pyyaml -pocketsphinx>=0.1.3 -coloredlogs -future>=0.16.0 \ No newline at end of file diff --git a/src/scripts/AlexaPi.service b/src/scripts/AlexaPi.service index 409665bd..0d5eb227 100644 --- a/src/scripts/AlexaPi.service +++ b/src/scripts/AlexaPi.service @@ -11,7 +11,7 @@ After=network.target network-online.target sound.target [Service] Type=simple -ExecStart=/usr/bin/python /opt/AlexaPi/src/main.py --daemon +ExecStart=/usr/bin/python3 /opt/AlexaPi/src/main.py --daemon User=alexapi Group=alexapi diff --git a/src/scripts/inc/common.sh b/src/scripts/inc/common.sh index 2d444f8c..1aa1f203 100644 --- a/src/scripts/inc/common.sh +++ b/src/scripts/inc/common.sh @@ -5,11 +5,11 @@ RUN_USER="alexapi" HOME_DIR="/var/lib/AlexaPi" function run_python { - python2 "$@" + python3 "$@" } function run_pip { - pip2 "$@" + pip3 "$@" } function init_classic { diff --git a/src/scripts/inc/os/archlinux.sh b/src/scripts/inc/os/archlinux.sh index 5e4a19c3..2b02e729 100644 --- a/src/scripts/inc/os/archlinux.sh +++ b/src/scripts/inc/os/archlinux.sh @@ -2,9 +2,8 @@ function install_os { pacman -Sy - pacman -S base-devel git python2 python2-pip swig portaudio libpulse vlc sox libmad libid3tag gcc --noconfirm --needed + pacman -S base-devel git python python-pip swig portaudio libpulse vlc sox libmad libid3tag gcc --noconfirm --needed - install -Dm644 ./unit-overrides/force-python2.conf /etc/systemd/system/AlexaPi.service.d/force-python2.conf systemctl daemon-reload } diff --git a/src/scripts/inc/os/debian.sh b/src/scripts/inc/os/debian.sh index 95794578..0bc94390 100644 --- a/src/scripts/inc/os/debian.sh +++ b/src/scripts/inc/os/debian.sh @@ -2,9 +2,7 @@ function install_os { apt-get update - apt-get install curl git build-essential python-dev python-setuptools swig libpulse-dev portaudio19-dev libportaudio2 vlc-nox sox libsox-fmt-mp3 -y - apt-get -y remove python-pip - run_python -m easy_install pip + apt-get install curl git build-essential python3-dev python3-pip python3-setuptools swig libpulse-dev portaudio19-dev libportaudio2 vlc-nox sox libsox-fmt-mp3 -y } function install_shairport-sync { diff --git a/src/scripts/initd_alexa.sh b/src/scripts/initd_alexa.sh index 9395edc2..3056b689 100755 --- a/src/scripts/initd_alexa.sh +++ b/src/scripts/initd_alexa.sh @@ -16,7 +16,7 @@ set -e NAME="AlexaPi" PIDFILE="/run/$NAME/$NAME.pid" -DAEMON="/usr/bin/python /opt/AlexaPi/src/main.py" +DAEMON="/usr/bin/python3 /opt/AlexaPi/src/main.py" DAEMON_OPTS="--daemon" RUN_USER="alexapi" RUN_GROUP="alexapi" diff --git a/src/scripts/setup.bat b/src/scripts/setup.bat index e46e2ef2..a09679da 100644 --- a/src/scripts/setup.bat +++ b/src/scripts/setup.bat @@ -12,7 +12,7 @@ echo ------------------------------------------- echo Installing dependencies: -python -m pip install -r "%apath%requirements.txt" +python3 -m pip install -r "%apath%\..\requirements.txt" pause cls @@ -27,7 +27,7 @@ echo You HAVE TO set up Amazon keys in the config.yaml file now echo ###################################################################################################### pause -start python.exe auth_web.py +start python3.exe auth_web.py echo ===== echo Done! diff --git a/src/scripts/setup.sh b/src/scripts/setup.sh index 48fc814d..57b7cac1 100755 --- a/src/scripts/setup.sh +++ b/src/scripts/setup.sh @@ -132,9 +132,9 @@ cd "${ALEXASRC_DIRECTORY}" # This is here because of https://github.com/pypa/pip/issues/2984 if run_pip --version | grep "pip 1.5"; then - run_pip install -r ./requirements.txt + run_pip install -r ../requirements.txt else - run_pip install --no-cache-dir -r ./requirements.txt + run_pip install --no-cache-dir -r ../requirements.txt fi install_device diff --git a/src/scripts/unit-overrides/force-python2.conf b/src/scripts/unit-overrides/force-python2.conf deleted file mode 100644 index 5f377c56..00000000 --- a/src/scripts/unit-overrides/force-python2.conf +++ /dev/null @@ -1,5 +0,0 @@ -# This is mainly for Arch Linux when using python2 - -[Service] -ExecStart= -ExecStart=/usr/bin/python2 /opt/AlexaPi/src/main.py --daemon \ No newline at end of file diff --git a/src/start.bat b/src/start.bat index 8f6725bc..705432ea 100644 --- a/src/start.bat +++ b/src/start.bat @@ -2,6 +2,6 @@ echo off title AlexaPi for Windows cls -python main.py +python3 main.py pause \ No newline at end of file