diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..db42018 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,27 @@ +engines: + stylelint: + enabled: true + duplication: + enabled: true + config: + languages: + - javascript + eslint: + enabled: true + channel: "eslint-3" + checks: + import/no-unresolved: + enabled: false + fixme: + enabled: true + markdownlint: + enabled: true +ratings: + paths: + - "**.js" + - "**.css" + - "**.md" +exclude_paths: [ + "node_modules/**/*", + "Bytes.js" +] diff --git a/.doclets.yml b/.doclets.yml new file mode 100644 index 0000000..a301381 --- /dev/null +++ b/.doclets.yml @@ -0,0 +1,10 @@ +dir: . +packageJson: package.json +articles: + - Overview: README.md + - Developer: DEVELOPER.md + - Changelog: CHANGELOG.md + - License: LICENSE +branches: + - master + - develop diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..15d51b0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 4 + +[{*.json, *.yml}] +indent_style = space +indent_size = 2 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..35f6bcc --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +Bytes.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..532082c --- /dev/null +++ b/.eslintrc @@ -0,0 +1,18 @@ +{ + "extends": "airbnb-base", + "rules": { + "comma-dangle": 0, + "indent": [2, 4], + "max-len": [2, 120, { "ignoreStrings": true }], + "radix": [2, "as-needed"], + "no-console": 0 + }, + "settings": { + "import/core-modules": [ "node_helper" ] + }, + "env": { + "browser": true, + "node": true, + "es6": true + } +} diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..5956f32 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,8 @@ +# Contribution Guidelines + +Thanks for contributing to this module! + +Please create pull requests to the branch `develop`. + +To hold one code style and standard there are several linters and tools in this project set. Make sure you fullfill the requirements. +Also there will be automatically analysis performed once you created the pull request. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..5bbae68 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,9 @@ +Platform (Hardware/OS): + +Node version: + +MagicMirror version: + +Module version: + +Description of the issue: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..63fa699 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ +Please create pull requests to the branch `develop`. + +* Does the pull request solve an issue (add a reference)? +* What are the features of this pr? +* Add screenshots for visual changes. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a19034 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.log +npm-debug.log* + +node_modules/ + +.idea/ + +docs/ diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 0000000..54d0111 --- /dev/null +++ b/.mdlrc @@ -0,0 +1,2 @@ +all +rules "~MD013", "~MD026", "~MD033" diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 0000000..6449c3f --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,6 @@ +{ + "extends": "stylelint-config-standard", + "rules": { + "indentation": 4 + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b89f2aa --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: node_js +node_js: + - "stable" + - "7" + - "6" + - "5" +script: + - npm run lint +cache: + directories: + - node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..75bf570 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# MMM-voice Changelog + +## [1.0.1] + +### Added + +* Code linter +* Documentation +* [Doclets.io](https://doclets.io/fewieden/MMM-voice/master) integration +* Contributing guidelines +* Issue template +* Pull request template +* Gitignore + +### Changed + +* Dependency manipulation exchanged to fork + +## [1.0.0] + +Initial version diff --git a/DEVELOPER.md b/DEVELOPER.md index 6a47bc9..cf5e3e9 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -3,18 +3,23 @@ This document describes the way to support your own MagicMirror² module with voice control. ## Mode + Use an unique mode, which is not already taken from one of the other modules in this [list](https://github.com/fewieden/MMM-voice/wiki/Supported-Modules). ## COMMANDS + Try to avoid short words like `ON`, `TO`, etc. as far as possible ## Register your commands + As soon as you receive the notification `ALL_MODULES_STARTED` from the core system, register your voice commands by sending the following notification - * notification: `REGISTER_VOICE_MODULE` - * payload: Object with `mode` (string) and `sentence` (array) properties -### Example -````javascript +* notification: `REGISTER_VOICE_MODULE` +* payload: Object with `mode` (string) and `sentence` (array) properties + +### Example commands registration + +```javascript notificationReceived: function (notification, payload, sender) { if(notification === "ALL_MODULES_STARTED"){ this.sendNotification("REGISTER_VOICE_MODULE", { @@ -28,15 +33,18 @@ notificationReceived: function (notification, payload, sender) { }); } } -```` +``` ## Handle recognized data + When the user is in the mode of your module, you will receive the following notification - * notification: `VOICE_YOURMODE` - * payload: String with all detected words. -### Example -````javascript +* notification: `VOICE_YOURMODE` +* payload: String with all detected words. + +### Example commands recognition + +```javascript notificationReceived: function (notification, payload, sender) { ... if(notification === "VOICE_FOOTBALL" && sender.name === "MMM-voice"){ @@ -51,21 +59,24 @@ checkCommands: function(data){ } ... } -```` +``` ## React on mode change + When the mode of MMM-voice gets changed it will send a broadcast `VOICE_MODE_CHANGED` - * notification: `VOICE_MODE_CHANGED` - * payload: Object with `old` (string) and `new` (string) mode as properties + +* notification: `VOICE_MODE_CHANGED` +* payload: Object with `old` (string) and `new` (string) mode as properties This gets handy e.g. to revert your manipulations on the DOM. -### Example -````javascript +### Example mode change + +```javascript notificationReceived: function (notification, payload, sender) { ... if(notification === "VOICE_MODE_CHANGED" && sender.name === "MMM-voice" && payload.old === "FOOTBALL"){ // do your magic } } -```` \ No newline at end of file +``` diff --git a/MMM-voice.css b/MMM-voice.css index 723f088..18fc5a2 100644 --- a/MMM-voice.css +++ b/MMM-voice.css @@ -1,11 +1,12 @@ @-webkit-keyframes MMM-voice-pulse { - 0% {color: #999999;} - 50% {color: #000000;} - 100% {color: #999999;} + 0% { color: #999; } + 50% { color: #000; } + 100% { color: #999; } } .MMM-voice .pulse { -webkit-animation: MMM-voice-pulse 1s infinite; + animation: MMM-voice-pulse 1s infinite; } .MMM-voice .icon { @@ -14,6 +15,7 @@ .MMM-voice-blur { -webkit-filter: blur(2px) brightness(50%); + filter: blur(2px) brightness(50%); } .MMM-voice .modal { @@ -22,8 +24,11 @@ left: 50%; top: 50%; -webkit-transform: translate(-50%, -50%); + -moz-transform: translate(-50%, -50%); + -o-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); } .MMM-voice .modal ul { - margin: 0px; -} \ No newline at end of file + margin: 0; +} diff --git a/MMM-voice.js b/MMM-voice.js index 2594461..a66da27 100644 --- a/MMM-voice.js +++ b/MMM-voice.js @@ -1,89 +1,159 @@ -/* Magic Mirror - * Module: MMM-voice +/** + * @file MMM-voice.js * - * By fewieden https://github.com/fewieden/MMM-voice - * MIT Licensed. + * @author fewieden + * @license MIT + * + * @see https://github.com/fewieden/MMM-voice + */ + +/* global Module Log MM */ + +/** + * @external Module + * @see https://github.com/MichMich/MagicMirror/blob/master/js/module.js + */ + +/** + * @external Log + * @see https://github.com/MichMich/MagicMirror/blob/master/js/logger.js */ -Module.register("MMM-voice",{ +/** + * @external MM + * @see https://github.com/MichMich/MagicMirror/blob/master/js/main.js + */ + +/** + * @module MMM-voice + * @description Frontend for the module to display data. + * + * @requires external:Module + * @requires external:Log + * @requires external:MM + */ +Module.register('MMM-voice', { - icon: "fa-microphone-slash", + /** @member {string} icon - Microphone icon. */ + icon: 'fa-microphone-slash', + /** @member {boolean} pulsing - Flag to indicate listening state. */ pulsing: true, + /** @member {boolean} help - Flag to switch between render help or not. */ help: false, + /** + * @member {Object} voice - Defines the default mode and commands of this module. + * @property {string} mode - Voice mode of this module. + * @property {string[]} sentences - List of voice commands of this module. + */ voice: { - mode: "VOICE", + mode: 'VOICE', sentences: [ - "HIDE MODULES", - "SHOW MODULES", - "WAKE UP", - "GO TO SLEEP", - "OPEN HELP", - "CLOSE HELP" + 'HIDE MODULES', + 'SHOW MODULES', + 'WAKE UP', + 'GO TO SLEEP', + 'OPEN HELP', + 'CLOSE HELP' ] }, + /** @member {Object[]} modules - Set of all modules with mode and commands. */ modules: [], + /** + * @member {Object} defaults - Defines the default config values. + * @property {int} timeout - Seconds to active listen for commands. + * @property {string} keyword - Keyword to activate active listening. + * @property {boolean} debug - Flag to enable debug information. + */ defaults: { timeout: 15, - keyword: "MAGIC MIRROR", + keyword: 'MAGIC MIRROR', debug: false }, - start: function(){ - this.mode = this.translate("INIT"); + /** + * @function start + * @description Sets mode to initialising. + * @override + */ + start() { + Log.info(`Starting module: ${this.name}`); + this.mode = this.translate('INIT'); this.modules.push(this.voice); - Log.log(this.name + " is started!"); - Log.info(this.name + " is waiting for voice modules"); + Log.info(`${this.name} is waiting for voice command registrations.`); }, - getStyles: function() { - return ["font-awesome.css", "MMM-voice.css"]; + /** + * @function getStyles + * @description Style dependencies for this module. + * @override + * + * @returns {string[]} List of the style dependency filepaths. + */ + getStyles() { + return ['font-awesome.css', 'MMM-voice.css']; }, - getTranslations: function() { + /** + * @function getTranslations + * @description Translations for this module. + * @override + * + * @returns {Object.} Available translations for this module (key: language code, value: filepath). + */ + getTranslations() { return { - en: "translations/en.json", - de: "translations/de.json", - id: "translations/id.json" + en: 'translations/en.json', + de: 'translations/de.json', + id: 'translations/id.json' }; }, - getDom: function() { - var wrapper = document.createElement("div"); - var voice = document.createElement("div"); - voice.classList.add("small", "align-left"); - var i = document.createElement("i"); - i.classList.add("fa", this.icon, "icon"); - if(this.pulsing){ - i.classList.add("pulse"); + /** + * @function getDom + * @description Creates the UI as DOM for displaying in MagicMirror application. + * @override + * + * @returns {Element} + */ + getDom() { + const wrapper = document.createElement('div'); + const voice = document.createElement('div'); + voice.classList.add('small', 'align-left'); + + const icon = document.createElement('i'); + icon.classList.add('fa', this.icon, 'icon'); + if (this.pulsing) { + icon.classList.add('pulse'); } - voice.appendChild(i); - var modeSpan = document.createElement("span"); + voice.appendChild(icon); + + const modeSpan = document.createElement('span'); modeSpan.innerHTML = this.mode; voice.appendChild(modeSpan); - if(this.config.debug){ - var debug = document.createElement("div"); + if (this.config.debug) { + const debug = document.createElement('div'); debug.innerHTML = this.debugInformation; voice.appendChild(debug); } - var modules = document.querySelectorAll(".module"); - for (var i = 0; i < modules.length; i++) { - if(!modules[i].classList.contains(this.name)){ - if(this.help){ - modules[i].classList.add(this.name + "-blur"); + const modules = document.querySelectorAll('.module'); + for (let i = 0; i < modules.length; i += 1) { + if (!modules[i].classList.contains(this.name)) { + if (this.help) { + modules[i].classList.add(`${this.name}-blur`); } else { - modules[i].classList.remove(this.name + "-blur"); + modules[i].classList.remove(`${this.name}-blur`); } } } - if(this.help){ - voice.classList.add(this.name + "-blur"); - var modal = document.createElement("div"); - modal.classList.add("modal"); + if (this.help) { + voice.classList.add(`${this.name}-blur`); + const modal = document.createElement('div'); + modal.classList.add('modal'); this.appendHelp(modal); wrapper.appendChild(modal); } @@ -93,81 +163,103 @@ Module.register("MMM-voice",{ return wrapper; }, - notificationReceived: function(notification, payload, sender){ - if(notification === "DOM_OBJECTS_CREATED"){ - this.sendSocketNotification("START", {config: this.config, modules: this.modules}); - } else if(notification === "REGISTER_VOICE_MODULE"){ - if(payload.hasOwnProperty("mode") && payload.hasOwnProperty("sentences")){ + /** + * @function notificationReceived + * @description Handles incoming broadcasts from other modules or the MagicMirror core. + * @override + * + * @param {string} notification - Notification name + * @param {*} payload - Detailed payload of the notification. + */ + notificationReceived(notification, payload) { + if (notification === 'DOM_OBJECTS_CREATED') { + this.sendSocketNotification('START', { config: this.config, modules: this.modules }); + } else if (notification === 'REGISTER_VOICE_MODULE') { + if (Object.prototype.hasOwnProperty.call(payload, 'mode') && Object.prototype.hasOwnProperty.call(payload, 'sentences')) { this.modules.push(payload); } } }, - socketNotificationReceived: function(notification, payload){ - if(notification === "READY"){ - this.icon = "fa-microphone"; - this.mode = this.translate("NO_MODE"); + /** + * @function socketNotificationReceived + * @description Handles incoming messages from node_helper. + * @override + * + * @param {string} notification - Notification name + * @param {*} payload - Detailed payload of the notification. + */ + socketNotificationReceived(notification, payload) { + if (notification === 'READY') { + this.icon = 'fa-microphone'; + this.mode = this.translate('NO_MODE'); this.pulsing = false; - } else if(notification === "LISTENING"){ + } else if (notification === 'LISTENING') { this.pulsing = true; - } else if(notification === "SLEEPING"){ + } else if (notification === 'SLEEPING') { this.pulsing = false; - } else if(notification === "ERROR"){ + } else if (notification === 'ERROR') { this.mode = notification; - } else if(notification === "VOICE"){ - for(var i = 0; i < this.modules.length; i++){ - if(payload.mode === this.modules[i].mode){ - if(this.mode !== payload.mode) { + } else if (notification === 'VOICE') { + for (let i = 0; i < this.modules.length; i += 1) { + if (payload.mode === this.modules[i].mode) { + if (this.mode !== payload.mode) { this.help = false; - this.sendNotification(notification + '_MODE_CHANGED', {old: this.mode, new: payload.mode}); + this.sendNotification(`${notification}_MODE_CHANGED`, { old: this.mode, new: payload.mode }); this.mode = payload.mode; } - if(this.mode !== "VOICE"){ - this.sendNotification(notification + '_' + payload.mode, payload.sentence); + if (this.mode !== 'VOICE') { + this.sendNotification(`${notification}_${payload.mode}`, payload.sentence); } break; } } - } else if(notification === "BYTES"){ - this.sendNotification("MMM-TTS", payload); - } else if(notification === "HIDE"){ + } else if (notification === 'BYTES') { + this.sendNotification('MMM-TTS', payload); + } else if (notification === 'HIDE') { MM.getModules().enumerate((module) => { module.hide(1000); }); - } else if(notification === "SHOW"){ + } else if (notification === 'SHOW') { MM.getModules().enumerate((module) => { module.show(1000); }); - } else if(notification === "OPEN_HELP"){ + } else if (notification === 'OPEN_HELP') { this.help = true; - } else if(notification === "CLOSE_HELP"){ + } else if (notification === 'CLOSE_HELP') { this.help = false; - } else if(notification === "DEBUG"){ + } else if (notification === 'DEBUG') { this.debugInformation = payload; } this.updateDom(300); }, - appendHelp: function(appendTo){ - var title = document.createElement("h1"); - title.classList.add("medium"); - title.innerHTML = this.name + " - " + this.translate("COMMAND_LIST"); + /** + * @function appendHelp + * @description Creates the UI for the voice command SHOW HELP. + * + * @param {Element} appendTo - DOM Element where the UI gets appended as child. + */ + appendHelp(appendTo) { + const title = document.createElement('h1'); + title.classList.add('medium'); + title.innerHTML = `${this.name} - ${this.translate('COMMAND_LIST')}`; appendTo.appendChild(title); - var mode = document.createElement("div"); - mode.innerHTML = this.translate("MODE") + ": " + this.voice.mode; + const mode = document.createElement('div'); + mode.innerHTML = `${this.translate('MODE')}: ${this.voice.mode}`; appendTo.appendChild(mode); - var listLabel = document.createElement("div"); - listLabel.innerHTML = this.translate("VOICE_COMMANDS") + ":"; + const listLabel = document.createElement('div'); + listLabel.innerHTML = `${this.translate('VOICE_COMMANDS')}:`; appendTo.appendChild(listLabel); - var list = document.createElement("ul"); - for(var i = 0; i < this.voice.sentences.length; i++){ - var item = document.createElement("li"); + const list = document.createElement('ul'); + for (let i = 0; i < this.voice.sentences.length; i += 1) { + const item = document.createElement('li'); item.innerHTML = this.voice.sentences[i]; list.appendChild(item); } appendTo.appendChild(list); } -}); \ No newline at end of file +}); diff --git a/README.md b/README.md index af6e2ff..f1222ce 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -# MMM-voice +# MMM-voice [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://raw.githubusercontent.com/fewieden/MMM-voice/master/LICENSE) [![Build Status](https://travis-ci.org/fewieden/MMM-voice.svg?branch=master)](https://travis-ci.org/fewieden/MMM-voice) [![Code Climate](https://codeclimate.com/github/fewieden/MMM-voice/badges/gpa.svg?style=flat)](https://codeclimate.com/github/fewieden/MMM-voice) [![Known Vulnerabilities](https://snyk.io/test/github/fewieden/mmm-voice/badge.svg)](https://snyk.io/test/github/fewieden/mmm-voice) [![API Doc](https://doclets.io/fewieden/MMM-voice/master.svg)](https://doclets.io/fewieden/MMM-voice/master) + Voice Recognition Module for MagicMirror2 ## Information + This voice recognition works offline. To protect your privacy, no one will record what's going on in your room all day long. So keep in mind that there is no huge server farm, that handles your voice commands. The raspberry is just a small device and this is a cpu intensive task. Also the dictionairy has only the words specified by the modules, so there is a chance for false positives. @@ -9,32 +11,35 @@ Also the dictionairy has only the words specified by the modules, so there is a If you can live with latency, bugged detections and want to have data privacy, feel free to use this module. ## Dependencies - * An installation of [MagicMirror2](https://github.com/MichMich/MagicMirror) - * Packages: bison libasound2-dev autoconf automake libtool python-dev swig python-pip - * [SphinxBase](https://github.com/cmusphinx/sphinxbase) - * [PocketSphinx](https://github.com/cmusphinx/pocketsphinx) - * A microphone - * npm - * [PocketSphinx-continuous](https://www.npmjs.com/package/pocketsphinx-continuous) - * [lmtool](https://www.npmjs.com/package/lmtool) + +* An installation of [MagicMirror2](https://github.com/MichMich/MagicMirror) +* Packages: bison libasound2-dev autoconf automake libtool python-dev swig python-pip +* [SphinxBase](https://github.com/cmusphinx/sphinxbase) +* [PocketSphinx](https://github.com/cmusphinx/pocketsphinx) +* A microphone +* npm +* [PocketSphinx-continuous](https://www.npmjs.com/package/pocketsphinx-continuous) +* [lmtool](https://www.npmjs.com/package/lmtool) ## Installation - 1. Clone this repo into `~/MagicMirror/modules` directory. - 2. Run command `bash dependencies.sh` in `~/MagicMirror/modules/MMM-voice/installers` directory, to install all dependencies. This will need a couple of minutes. - 3. Configure your `~/MagicMirror/config/config.js`: - - ``` - { - module: 'MMM-voice', - position: 'bottom_bar', - config: { - microphone: 1, - ... - } - } - ``` + +1. Clone this repo into `~/MagicMirror/modules` directory. +1. Run command `bash dependencies.sh` in `~/MagicMirror/modules/MMM-voice/installers` directory, to install all dependencies. This will need a couple of minutes. +1. Configure your `~/MagicMirror/config/config.js`: + + ``` + { + module: 'MMM-voice', + position: 'bottom_bar', + config: { + microphone: 1, + ... + } + } + ``` ## Config Options + | **Option** | **Default** | **Description** | | --- | --- | --- | | `microphone` | REQUIRED | Id of microphone shown in the installer. | @@ -42,23 +47,37 @@ If you can live with latency, bugged detections and want to have data privacy, f | `timeout` | `15` | time the keyword should be active without saying something | ## Usage + You need to say your KEYWORD (Default: MAGIC MIRROR), when the KEYWORD is recognized the microphone will start to flash and as long as the microphone is flashing (timeout config option) the mirror will recognize COMMANDS or MODES (Keep in mind that the recognition will take a while, so when you say your COMMAND right before the microphone stops flashing the COMMAND will propably not recognized). Mode of this module: `VOICE` -COMMANDS: - * HIDE MODULES - * SHOW MODULES - * WAKE UP - * GO TO SLEEP - * OPEN HELP - * CLOSE HELP +COMMANDS: + +* HIDE MODULES +* SHOW MODULES +* WAKE UP +* GO TO SLEEP +* OPEN HELP +* CLOSE HELP ### Select Mode + To select a MODE, the specfic MODE has to be the first word of a COMMAND or right after the KEYWORD, when the microphone stopped flashing. ## Supported modules + List of all supported modules in the [Wiki](https://github.com/fewieden/MMM-voice/wiki/Supported-Modules). -## Developers Guide -If you want to support your own module, check out the [Documentation](https://github.com/fewieden/MMM-voice/blob/master/DEVELOPER.md) and add it to the [Wiki](https://github.com/fewieden/MMM-voice/wiki/Supported-Modules). +## Developer + +* `npm run lint` - Lints JS and CSS files. +* `npm run docs` - Generates documentation. + +### Documentation + +The documentation can be found [here](https://doclets.io/fewieden/MMM-voice/master) + +### Developers Guide + +If you want to support your own module, check out the [Guide](DEVELOPER.md) and add it to the [Wiki](https://github.com/fewieden/MMM-voice/wiki/Supported-Modules). diff --git a/installers/dependencies.sh b/installers/dependencies.sh index f24080d..9856467 100644 --- a/installers/dependencies.sh +++ b/installers/dependencies.sh @@ -32,24 +32,24 @@ echo -e "\e[0m" # installing packages -echo -e "\e[96m[STEP 1/6] Installing Packages\e[90m" +echo -e "\e[96m[STEP 1/5] Installing Packages\e[90m" if sudo apt-get install bison libasound2-dev autoconf automake libtool python-dev swig python-pip -y ; then - echo -e "\e[32m[STEP 1/6] Installing Packages | Done\e[0m" + echo -e "\e[32m[STEP 1/5] Installing Packages | Done\e[0m" else - echo -e "\e[31m[STEP 1/6] Installing Packages | Failed\e[0m" + echo -e "\e[31m[STEP 1/5] Installing Packages | Failed\e[0m" exit; fi # installing sphinxbase -echo -e "\e[96m[STEP 2/6] Installing sphinxbase\e[90m" +echo -e "\e[96m[STEP 2/5] Installing sphinxbase\e[90m" cd ~ if [ ! -d "$HOME/sphinxbase" ] ; then if ! git clone https://github.com/cmusphinx/sphinxbase.git ; then - echo -e "\e[31m[STEP 2/6] Installing sphinxbase | Failed\e[0m" + echo -e "\e[31m[STEP 2/5] Installing sphinxbase | Failed\e[0m" exit; fi fi @@ -57,7 +57,7 @@ fi cd sphinxbase if ! git pull ; then - echo -e "\e[31m[STEP 2/6] Installing sphinxbase | Failed\e[0m" + echo -e "\e[31m[STEP 2/5] Installing sphinxbase | Failed\e[0m" exit; fi @@ -65,17 +65,17 @@ fi ./configure --enable-fixed make sudo make install -echo -e "\e[32m[STEP 2/6] Installing sphinxbase | Done\e[0m" +echo -e "\e[32m[STEP 2/5] Installing sphinxbase | Done\e[0m" # installing pocketsphinx -echo -e "\e[96m[STEP 3/6] Installing pocketsphinx\e[90m" +echo -e "\e[96m[STEP 3/5] Installing pocketsphinx\e[90m" cd ~ if [ ! -d "$HOME/pocketsphinx" ] ; then if ! git clone https://github.com/cmusphinx/pocketsphinx.git ; then - echo -e "\e[31m[STEP 3/6] Installing pocketsphinx | Failed\e[0m" + echo -e "\e[31m[STEP 3/5] Installing pocketsphinx | Failed\e[0m" exit; fi fi @@ -83,7 +83,7 @@ fi cd pocketsphinx if ! git pull ; then - echo -e "\e[31m[STEP 3/6] Installing pocketsphinx | Failed\e[0m" + echo -e "\e[31m[STEP 3/5] Installing pocketsphinx | Failed\e[0m" exit; fi @@ -91,46 +91,28 @@ fi ./configure make sudo make install -echo -e "\e[32m[STEP 3/6] Installing pocketsphinx | Done\e[0m" +echo -e "\e[32m[STEP 3/5] Installing pocketsphinx | Done\e[0m" # exporting paths -echo -e "\e[96m[STEP 4/6] Exporting paths\e[0m" +echo -e "\e[96m[STEP 4/5] Exporting paths\e[0m" echo "export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib" >> ~/.bashrc echo "export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig" >> ~/.bashrc -echo -e "\e[32m[STEP 4/6] Exporting paths | Done\e[0m" +echo -e "\e[32m[STEP 4/5] Exporting paths | Done\e[0m" # installing npm dependencies -echo -e "\e[96m[STEP 5/6] Installing npm dependencies\e[90m" +echo -e "\e[96m[STEP 5/5] Installing npm dependencies\e[90m" cd ~/MagicMirror/modules/MMM-voice -if npm install ; +if npm install --productive; then - echo -e "\e[32m[STEP 5/6] Installing npm dependencies | Done\e[0m" + echo -e "\e[32m[STEP 5/5] Installing npm dependencies | Done\e[0m" else - echo -e "\e[31m[STEP 5/6] Installing npm dependencies | Failed\e[0m" - exit; -fi - - -# manipulating dependencies -echo -e "\e[96m[STEP 6/6] Manipulating dependencies\e[90m" -cd ~/MagicMirror/modules/MMM-voice/node_modules/pocketsphinx-continuous -if sed \ --e "/this.verbose = config.verbose;/ a\ this.microphone = config.microphone;" \ --e "/-inmic/ i\ '-adcdev'," \ --e "/-inmic/ i\ 'plughw:' \+ this.microphone," \ --e "/-lm/ a\ 'modules/MMM-voice/' \+" \ --e "/-dict/ a\ 'modules/MMM-voice/' \+" \ -index.js -i; -then - echo -e "\e[32m[STEP 6/6] Manipulating dependencies | Done\e[0m" -else - echo -e "\e[31m[STEP 6/6] Manipulating dependencies | Failed\e[0m" + echo -e "\e[31m[STEP 5/5] Installing npm dependencies | Failed\e[0m" exit; fi # displaying audio devices -echo -e "\e[96m[INFO] Possible Audio Devices to set in config.js\n" -cat /proc/asound/cards \ No newline at end of file +echo -e "\e[96m[INFO] Possible Audio Devices to set in config.js\n\e[0m" +cat /proc/asound/cards diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 0000000..5c73e9f --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,19 @@ +{ + "tags": { + "dictionaries": ["jsdoc"] + }, + "source": { + "include": [ + "package.json", + "README.md" + ], + "exclude": [ + "node_modules", + "Bytes.js" + ] + }, + "opts": { + "destination": "docs", + "recurse": true + } +} diff --git a/node_helper.js b/node_helper.js index 363ffb0..f219dfe 100644 --- a/node_helper.js +++ b/node_helper.js @@ -1,28 +1,100 @@ -/* Magic Mirror - * Module: MMM-voice +/** + * @file node_helper.js * - * By fewieden https://github.com/fewieden/MMM-voice - * MIT Licensed. + * @author fewieden + * @license MIT + * + * @see https://github.com/fewieden/MMM-voice + */ + +/** + * @external pocketsphinx-continuous + * @see https://github.com/fewieden/pocketsphinx-continuous-node */ +const Psc = require('pocketsphinx-continuous'); -const Psc = require("pocketsphinx-continuous"); -const fs = require("fs"); -const exec = require("child_process").exec; -const lmtool = require("lmtool"); -const bytes = require("./Bytes.js"); -const NodeHelper = require("node_helper"); +/** + * @external fs + * @see https://nodejs.org/api/fs.html + */ +const fs = require('fs'); +/** + * @external child_process + * @see https://nodejs.org/api/child_process.html + */ +const exec = require('child_process').exec; + +/** + * @external lmtool + * @see https://www.npmjs.com/package/lmtool + */ +const lmtool = require('lmtool'); + +/** + * @module Bytes + * @description Pure Magic + */ +const bytes = require('./Bytes.js'); + +/** + * @external node_helper + * @see https://github.com/MichMich/MagicMirror/blob/master/modules/node_modules/node_helper/index.js + */ +const NodeHelper = require('node_helper'); + +/** + * @module node_helper + * @description Backend for the module to query data from the API providers. + * + * @requires external:pocketsphinx-continuous + * @requires external:fs + * @requires external:child_process + * @requires external:lmtool + * @requires Bytes + * @requires external:node_helper + */ module.exports = NodeHelper.create({ + /** @member {boolean} listening - Flag to indicate listen state. */ listening: false, + + /** @member {(boolean|string)} mode - Contains active module mode. */ mode: false, + + /** @member {boolean} hdmi - Flag to indicate hdmi output state. */ hdmi: true, + + /** @member {boolean} help - Flag to toggle help modal. */ help: false, + + /** @member {string[]} words - List of all words that are registered by the modules. */ words: [], - socketNotificationReceived: function(notification, payload){ - if(notification === "START"){ + /** + * @function start + * @description Logs a start message to the console. + * @override + */ + start() { + console.log(`Starting module helper: ${this.name}`); + }, + + /** + * @function socketNotificationReceived + * @description Receives socket notifications from the module. + * @override + * + * @param {string} notification - Notification name + * @param {*} payload - Detailed payload of the notification. + */ + socketNotificationReceived(notification, payload) { + if (notification === 'START') { + /** @member {Object} config - Module config. */ this.config = payload.config; + /** @member {number} time - Time to listen after keyword. */ + this.time = this.config.timeout * 1000; + /** @member {Object} modules - List of modules with their modes and commands. */ this.modules = payload.modules; this.fillWords(); @@ -30,50 +102,46 @@ module.exports = NodeHelper.create({ } }, - fillWords: function(){ - //create array - var array = this.config.keyword.split(" "); - var temp = bytes.q.split(" "); - for(var i = 0; i < temp.length; i++){ - array.push(temp[i]); - } - for(var i = 0; i < this.modules.length; i++){ - var mode = this.modules[i].mode.split(" "); - for(var m = 0; m < mode.length; m++){ - array.push(mode[m]); - } - for(var n = 0; n < this.modules[i].sentences.length; n++){ - var sentence = this.modules[i].sentences[n].split(" "); - for(var x = 0; x < sentence.length; x++){ - array.push(sentence[x]); - } + /** + * @function fillwords + * @description Sets {@link node_helper.words} with all needed words for the registered + * commands by the modules. This list has unique items and is sorted by alphabet. + */ + fillWords() { + // create array + let words = this.config.keyword.split(' '); + const temp = bytes.q.split(' '); + words = words.concat(temp); + for (let i = 0; i < this.modules.length; i += 1) { + const mode = this.modules[i].mode.split(' '); + words = words.concat(mode); + for (let n = 0; n < this.modules[i].sentences.length; n += 1) { + const sentences = this.modules[i].sentences[n].split(' '); + words = words.concat(sentences); } } - // sort array - array.sort(); + // filter duplicates + words = words.filter((item, index, data) => data.indexOf(item) === index); - //filter duplicates - var i = 0; - while(i < array.length) { - if(array[i] === array[i+1]) { - array.splice(i+1,1); - } else { - i += 1; - } - } + // sort array + words.sort(); - this.words = array; + this.words = words; }, - checkFiles: function(){ - console.log("MMM-voice: Checking files."); - fs.stat("modules/MMM-voice/words.json", (err, stats) => { - if(!err && stats.isFile()){ - fs.readFile("modules/MMM-voice/words.json", "utf8", (err, data) => { - if(!err){ - var words = JSON.parse(data).words; - if(this.arraysEqual(this.words, words)){ + /** + * @function checkFiles + * @description Checks if words.json exists or has different entries as this.word. + */ + checkFiles() { + console.log(`${this.name}: Checking files.`); + fs.stat('modules/MMM-voice/words.json', (error, stats) => { + if (!error && stats.isFile()) { + fs.readFile('modules/MMM-voice/words.json', 'utf8', (err, data) => { + if (!err) { + const words = JSON.parse(data).words; + if (this.arraysEqual(this.words, words)) { this.startPocketsphinx(); return; } @@ -86,16 +154,24 @@ module.exports = NodeHelper.create({ }); }, - arraysEqual: function(a, b){ - if(! (a instanceof Array) || ! (b instanceof Array)){ + /** + * @function arraysEqual + * @description Compares two arrays. + * + * @param {string[]} a - First array + * @param {string[]} b - Second array + * @returns {boolean} Are the arrays equal or not. + */ + arraysEqual(a, b) { + if (!(a instanceof Array) || !(b instanceof Array)) { return false; } - if(a.length !== b.length){ + if (a.length !== b.length) { return false; } - for (var i = 0; i < a.length; i++) { + for (let i = 0; i < a.length; i += 1) { if (a[i] !== b[i]) { return false; } @@ -104,129 +180,182 @@ module.exports = NodeHelper.create({ return true; }, - generateDicLM: function(){ - console.log("MMM-voice: Generating dictionairy and language model."); + /** + * @function generateDicLM + * @description Generates new Dictionairy and Language Model. + */ + generateDicLM() { + console.log(`${this.name}: Generating dictionairy and language model.`); - fs.writeFile("modules/MMM-voice/words.json", JSON.stringify({words: this.words}), (err) => { - if (err){ - console.log("MMM-voice: Couldn't save words.json!"); + fs.writeFile('modules/MMM-voice/words.json', JSON.stringify({ words: this.words }), (err) => { + if (err) { + console.log(`${this.name}: Couldn't save words.json!`); } else { - console.log("MMM-voice: Saved words.json successfully."); + console.log(`${this.name}: Saved words.json successfully.`); } }); lmtool(this.words, (err, filename) => { - if(err){ - this.sendSocketNotification("ERROR", "Couldn't create necessary files!"); + if (err) { + this.sendSocketNotification('ERROR', 'Couldn\'t create necessary files!'); } else { - fs.renameSync(filename + ".dic", "modules/MMM-voice/MMM-voice.dic"); - fs.renameSync(filename + ".lm", "modules/MMM-voice/MMM-voice.lm"); + fs.renameSync(`${filename}.dic`, 'modules/MMM-voice/MMM-voice.dic'); + fs.renameSync(`${filename}.lm`, 'modules/MMM-voice/MMM-voice.lm'); this.startPocketsphinx(); - fs.unlink(filename + ".log_pronounce"); - fs.unlink(filename + ".sent"); - fs.unlink(filename + ".vocab"); - fs.unlink("TAR" + filename + ".tgz"); + fs.unlink(`${filename}.log_pronounce`); + fs.unlink(`${filename}.sent`); + fs.unlink(`${filename}.vocab`); + fs.unlink(`TAR${filename}.tgz`); } }); }, - startPocketsphinx: function(){ - console.log("MMM-voice: Starting pocketsphinx."); - this.time = this.config.timeout * 1000; + /** + * @function startPocketsphinx + * @description Starts Pocketsphinx binary. + */ + startPocketsphinx() { + console.log(`${this.name}: Starting pocketsphinx.`); + this.ps = new Psc({ - setId: "MMM-voice", + setId: this.name, verbose: true, microphone: this.config.microphone }); - this.ps.on("data", (data) => { - if(typeof data === "string"){ - if(this.config.debug){ - console.log(data); - this.sendSocketNotification("DEBUG", data); - } - if(data.indexOf(this.config.keyword) !== -1 || this.listening){ - this.listening = true; - this.sendSocketNotification("LISTENING"); - if(this.timer){ - clearTimeout(this.timer); - } - this.timer = setTimeout(() => { - this.listening = false; - this.sendSocketNotification("SLEEPING"); - }, this.time); - } else { - return; - } + this.ps.on('data', this.handleData); - data = this.cleanData(data); + if (this.config.debug) { + this.ps.on('debug', this.logDebug); + } - for(var i = 0; i < this.modules.length; i++){ - var n = data.indexOf(this.modules[i].mode); - if(n === 0){ - this.mode = this.modules[i].mode; - data = data.substr(n + this.modules[i].mode.length).trim(); - break; - } + this.ps.on('error', this.logError); + + this.sendSocketNotification('READY'); + }, + + /** + * @function handleData + * @description Helper method to handle recognized data. + * + * @param {string} data - Recognized data + */ + handleData(data) { + if (typeof data === 'string') { + if (this.config.debug) { + console.log(`${this.name} has recognized: ${data}`); + this.sendSocketNotification('DEBUG', data); + } + if (data.includes(this.config.keyword) || this.listening) { + this.listening = true; + this.sendSocketNotification('LISTENING'); + if (this.timer) { + clearTimeout(this.timer); } + this.timer = setTimeout(() => { + this.listening = false; + this.sendSocketNotification('SLEEPING'); + }, this.time); + } else { + return; + } - if(this.mode){ - this.sendSocketNotification("VOICE", {mode: this.mode, sentence: data}); - if(this.mode === "VOICE"){ - this.checkCommands(data); - } + let cleanData = this.cleanData(data); + + for (let i = 0; i < this.modules.length; i += 1) { + const n = cleanData.indexOf(this.modules[i].mode); + if (n === 0) { + this.mode = this.modules[i].mode; + cleanData = cleanData.substr(n + this.modules[i].mode.length).trim(); + break; } } - }); - if(this.config.debug){ - this.ps.on("debug", (data) => { - fs.appendFile("modules/MMM-voice/debug.log", data); - }); + if (this.mode) { + this.sendSocketNotification('VOICE', { mode: this.mode, sentence: cleanData }); + if (this.mode === 'VOICE') { + this.checkCommands(cleanData); + } + } } + }, - this.ps.on("error", (error) => { - if(error){ - fs.appendFile("modules/MMM-voice/error.log", error); - this.sendSocketNotification("ERROR", error); + /** + * @function logDebug + * @description Logs debug information into debug log file. + * + * @param {string} data - Debug information + */ + logDebug(data) { + fs.appendFile('modules/MMM-voice/debug.log', data, (err) => { + if (err) { + console.log(`${this.name}: Couldn't save error to log file!`); } }); + }, - this.sendSocketNotification("READY"); + /** + * @function logError + * @description Logs error information into error log file. + * + * @param {string} data - Error information + */ + logError(error) { + if (error) { + fs.appendFile('modules/MMM-voice/error.log', `${error}\n`, (err) => { + if (err) { + console.log(`${this.name}: Couldn't save error to log file!`); + } + this.sendSocketNotification('ERROR', error); + }); + } }, - cleanData: function(data){ - var i = data.indexOf(this.config.keyword); + /** + * @function cleanData + * @description Removes prefix/keyword and multiple spaces. + * + * @param {string} data - Recognized data to clean. + * @returns {string} Cleaned data + */ + cleanData(data) { + let temp = data; + const i = temp.indexOf(this.config.keyword); if (i !== -1) { - data = data.substr(i + this.config.keyword.length); + temp = temp.substr(i + this.config.keyword.length); } - data = data.replace(/ +/g, " ").trim(); - return data; + temp = temp.replace(/ {2,}/g, ' ').trim(); + return temp; }, - checkCommands: function(data){ - if(bytes.r[0].test(data) && bytes.r[1].test(data)){ - this.sendSocketNotification("BYTES", bytes.a); - } else if(/(WAKE)/g.test(data) && /(UP)/g.test(data)){ - exec("/opt/vc/bin/tvservice -p && sudo chvt 6 && sudo chvt 7", null); + /** + * @function checkCommands + * @description Checks for commands of voice module + * @param {string} data - Recognized data + */ + checkCommands(data) { + if (bytes.r[0].test(data) && bytes.r[1].test(data)) { + this.sendSocketNotification('BYTES', bytes.a); + } else if (/(WAKE)/g.test(data) && /(UP)/g.test(data)) { + exec('/opt/vc/bin/tvservice -p && sudo chvt 6 && sudo chvt 7', null); this.hdmi = true; - } else if(/(GO)/g.test(data) && /(SLEEP)/g.test(data)){ - exec("/opt/vc/bin/tvservice -o", null); + } else if (/(GO)/g.test(data) && /(SLEEP)/g.test(data)) { + exec('/opt/vc/bin/tvservice -o', null); this.hdmi = false; - } else if(/(SHOW)/g.test(data) && /(MODULES)/g.test(data)){ - this.sendSocketNotification("SHOW"); - } else if(/(HIDE)/g.test(data) && /(MODULES)/g.test(data)){ - this.sendSocketNotification("HIDE"); - } else if(/(HELP)/g.test(data)){ - if(/(CLOSE)/g.test(data) || this.help && !/(OPEN)/g.test(data)){ - this.sendSocketNotification("CLOSE_HELP"); + } else if (/(SHOW)/g.test(data) && /(MODULES)/g.test(data)) { + this.sendSocketNotification('SHOW'); + } else if (/(HIDE)/g.test(data) && /(MODULES)/g.test(data)) { + this.sendSocketNotification('HIDE'); + } else if (/(HELP)/g.test(data)) { + if (/(CLOSE)/g.test(data) || (this.help && !/(OPEN)/g.test(data))) { + this.sendSocketNotification('CLOSE_HELP'); this.help = false; - } else if(/(OPEN)/g.test(data) || !this.help && !/(CLOSE)/g.test(data)){ - this.sendSocketNotification("OPEN_HELP"); + } else if (/(OPEN)/g.test(data) || (!this.help && !/(CLOSE)/g.test(data))) { + this.sendSocketNotification('OPEN_HELP'); this.help = true; } } } -}); \ No newline at end of file +}); diff --git a/package.json b/package.json index aba271d..0931fcf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,11 @@ { "name": "mmm-voice", - "version": "1.0.0", + "version": "1.0.1", "description": "Voice Recognition Module for MagicMirror2", + "scripts": { + "lint": "./node_modules/.bin/eslint . && ./node_modules/.bin/stylelint .", + "docs": "./node_modules/.bin/jsdoc -c jsdoc.json ." + }, "repository": { "type": "git", "url": "git+https://github.com/fewieden/MMM-voice.git" @@ -17,8 +21,16 @@ "url": "https://github.com/fewieden/MMM-voice/issues" }, "homepage": "https://github.com/fewieden/MMM-voice#readme", + "devDependencies": { + "eslint": "^3.14.1", + "eslint-config-airbnb-base": "^11.0.1", + "eslint-plugin-import": "^2.2.0", + "jsdoc": "^3.4.3", + "stylelint": "^7.8.0", + "stylelint-config-standard": "^16.0.0" + }, "dependencies": { "lmtool": "2.0.3", - "pocketsphinx-continuous": "1.1.0" + "pocketsphinx-continuous": "git://github.com/fewieden/pocketsphinx-continuous-node.git" } -} \ No newline at end of file +}