//! annyang //! version : 1.1.0 //! author : Tal Ater @TalAter //! license : MIT //! https://www.TalAter.com/annyang/ (function (undefined) { "use strict"; // Save a reference to the global object (window in the browser) var root = this; // Get the SpeechRecognition object, while handling browser prefixes var SpeechRecognition = root.SpeechRecognition || root.webkitSpeechRecognition || root.mozSpeechRecognition || root.msSpeechRecognition || root.oSpeechRecognition; // Check browser support // This is done as early as possible, to make it as fast as possible for unsupported browsers if (!SpeechRecognition) { root.annyang = null; return undefined; } var commandsList = []; var recognition; var callbacks = { start: [], error: [], end: [], result: [], resultMatch: [], resultNoMatch: [], errorNetwork: [], errorPermissionBlocked: [], errorPermissionDenied: [] }; var autoRestart; var lastStartedAt = 0; var debugState = false; var debugStyle = 'font-weight: bold; color: #00f;'; // The command matching code is a modified version of Backbone.Router by Jeremy Ashkenas, under the MIT license. var optionalParam = /\s*\((.*?)\)\s*/g; var optionalRegex = /(\(\?:[^)]+\))\?/g; var namedParam = /(\(\?)?:\w+/g; var splatParam = /\*\w+/g; var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#]/g; var commandToRegExp = function(command) { command = command.replace(escapeRegExp, '\\$&') .replace(optionalParam, '(?:$1)?') .replace(namedParam, function(match, optional) { return optional ? match : '([^\\s]+)'; }) .replace(splatParam, '(.*?)') .replace(optionalRegex, '\\s*$1?\\s*'); return new RegExp('^' + command + '$', 'i'); }; // This method receives an array of callbacks to iterate over, and invokes each of them var invokeCallbacks = function(callbacks) { callbacks.forEach(function(callback) { callback.callback.apply(callback.context); }); }; var initIfNeeded = function() { if (recognition === undefined) { root.annyang.init({}, false); } }; root.annyang = { // Initialize annyang with a list of commands to recognize. // e.g. annyang.init({'hello :name': helloFunction}) // annyang understands commands with named variables, splats, and optional words. init: function(commands, resetCommands) { // resetCommands defaults to true if (resetCommands === undefined) { resetCommands = true; } else { resetCommands = !!resetCommands; } try { // Abort previous instances of recognition already running if (recognition && recognition.abort) { recognition.abort(); } // initiate SpeechRecognition recognition = new SpeechRecognition(); // Set the max number of alternative transcripts to try and match with a command recognition.maxAlternatives = 5; recognition.continuous = true; // Sets the language to the default 'en-US'. This can be changed with annyang.setLanguage() recognition.lang = 'en-US'; } catch(err) { root.annyang = null; return undefined; } recognition.onstart = function() { invokeCallbacks(callbacks.start); }; recognition.onerror = function(event) { invokeCallbacks(callbacks.error); switch (event.error) { case 'network': invokeCallbacks(callbacks.errorNetwork); break; case 'not-allowed': case 'service-not-allowed': // if permission to use the mic is denied, turn off auto-restart autoRestart = false; // determine if permission was denied by user or automatically. if (new Date().getTime()-lastStartedAt < 200) { invokeCallbacks(callbacks.errorPermissionBlocked); } else { invokeCallbacks(callbacks.errorPermissionDenied); } break; } }; recognition.onend = function() { invokeCallbacks(callbacks.end); // annyang will auto restart if it is closed automatically and not by user action. if (autoRestart) { // play nicely with the browser, and never restart annyang automatically more than once per second var timeSinceLastStart = new Date().getTime()-lastStartedAt; if (timeSinceLastStart < 1000) { setTimeout(root.annyang.start, 1000-timeSinceLastStart); } else { root.annyang.start(); } } }; recognition.onresult = function(event) { invokeCallbacks(callbacks.result); var results = event.results[event.resultIndex]; var commandText; // go over each of the 5 results and alternative results received (we've set maxAlternatives to 5 above) for (var i = 0; i<results.length; i++) { // the text recognized commandText = results[i].transcript.trim(); if (debugState) { root.console.log('Speech recognized: %c'+commandText, debugStyle); } // try and match recognized text to one of the commands on the list for (var j = 0, l = commandsList.length; j < l; j++) { var result = commandsList[j].command.exec(commandText); if (result) { var parameters = result.slice(1); if (debugState) { root.console.log('command matched: %c'+commandsList[j].originalPhrase, debugStyle); if (parameters.length) { root.console.log('with parameters', parameters); } } // execute the matched command commandsList[j].callback.apply(this, parameters); invokeCallbacks(callbacks.resultMatch); return true; } } } invokeCallbacks(callbacks.resultNoMatch); return false; }; // build commands list if (resetCommands) { commandsList = []; } if (commands.length) { this.addCommands(commands); } }, // Start listening (asking for permission first, if needed). // Call this after you've initialized annyang with commands. // Receives an optional options object: // { autoRestart: true } start: function(options) { initIfNeeded(); options = options || {}; if (options.autoRestart !== undefined) { autoRestart = !!options.autoRestart; } else { autoRestart = true; } lastStartedAt = new Date().getTime(); recognition.start(); }, // abort the listening session (aka stop) abort: function() { initIfNeeded(); autoRestart = false; recognition.abort(); }, // Turn on output of debug messages to the console. Ugly, but super-handy! debug: function(newState) { if (arguments.length > 0) { debugState = !!newState; } else { debugState = true; } }, // Set the language the user will speak in. If not called, defaults to 'en-US'. // e.g. 'fr-FR' (French-France), 'es-CR' (EspaƱol-Costa Rica) setLanguage: function(language) { initIfNeeded(); recognition.lang = language; }, setContinuous: function(cont) { initIfNeeded(); recognition.continuous=cont; }, // Add additional commands that annyang will respond to. Similar in syntax to annyang.init() addCommands: function(commands) { var cb, command; initIfNeeded(); for (var phrase in commands) { if (commands.hasOwnProperty(phrase)) { cb = root[commands[phrase]] || commands[phrase]; if (typeof cb !== 'function') { continue; } //convert command to regex command = commandToRegExp(phrase); commandsList.push({ command: command, callback: cb, originalPhrase: phrase }); } } if (debugState) { root.console.log('Commands successfully loaded: %c'+commandsList.length, debugStyle); } }, // Remove existing commands. Called with a single phrase or an array of phrases removeCommands: function(commandsToRemove) { commandsToRemove = Array.isArray(commandsToRemove) ? commandsToRemove : [commandsToRemove]; commandsList = commandsList.filter(function(command) { for (var i = 0; i<commandsToRemove.length; i++) { if (commandsToRemove[i] === command.originalPhrase) { return false; } } return true; }); }, // Lets the user add a callback of one of 9 types: // start, error, end, result, resultMatch, resultNoMatch, errorNetwork, errorPermissionBlocked, errorPermissionDenied // Can also optionally receive a context for the callback function as the third argument addCallback: function(type, callback, context) { if (callbacks[type] === undefined) { return; } var cb = root[callback] || callback; if (typeof cb !== 'function') { return; } callbacks[type].push({callback: cb, context: context || this}); } }; }).call(this);