UNPKG

artyom.js

Version:

Artyom is a Robust Wrapper of the Google Chrome SpeechSynthesis and SpeechRecognition that allows you to create a virtual assistent

1,346 lines (1,145 loc) 64.2 kB
/** * Artyom.js is a voice control, speech recognition and speech synthesis JavaScript library. * * @requires {webkitSpeechRecognition && speechSynthesis} * @license MIT * @version 1.0.6 * @copyright 2017 Our Code World (www.ourcodeworld.com) All Rights Reserved. * @author Carlos Delgado (https://github.com/sdkcarlos) and Sema García (https://github.com/semagarcia) * @see https://sdkcarlos.github.io/sites/artyom.html * @see http://docs.ourcodeworld.com/projects/artyom-js */ /// <reference path="artyom.d.ts" /> // Remove "export default " keywords if willing to build with `npm run artyom-build-window` export default class Artyom { /** * Stores an object with all the available (or desired) voices for WebkitSpeechSynthesis */ private ArtyomVoicesIdentifiers: Object; /** * Stores the webkitSpeechRecognition instance used by Artyom. */ private ArtyomWebkitSpeechRecognition: any; /** * The initial default voice of Artyom. By default the UK Male English. */ private ArtyomVoice: ArtyomVoice; /** * An array that stores all the commands stored in the instance of Artyom */ private ArtyomCommands : Array<ArtyomCommand>; /** * Due to problems with the javascript garbage collector the SpeechSynthesisUtterance object * onEnd event doesn't get triggered sometimes. Therefore we need to keep the reference of the * object inside this global array variable. * * @see https://bugs.chromium.org/p/chromium/issues/detail?id=509488 */ private ArtyomGarbageCollection : Array<any>; /** * Object that stores some flags used by some if statements. */ private ArtyomFlags : ArtyomFlags; /** * Stores the default configuration of artyom. */ private ArtyomProperties : ArtyomProperties; /** * Object that stores some events identifiers */ private ArtyomGlobalEvents : ArtyomGlobalEvents; /** * Object that stores 2 single properties */ private Device : IDevice; // Triggered at the declaration of constructor() { this.ArtyomCommands = []; this.ArtyomVoicesIdentifiers = { // German "de-DE": ["Google Deutsch","de-DE","de_DE"], // Spanish "es-ES": ["Google español","es-ES", "es_ES","es-MX","es_MX"], // Italian "it-IT" : ["Google italiano","it-IT","it_IT"], // Japanese "jp-JP": ["Google 日本人","ja-JP","ja_JP"], // English USA "en-US": ["Google US English","en-US","en_US"], // English UK "en-GB": ["Google UK English Male","Google UK English Female","en-GB","en_GB"], // Brazilian Portuguese "pt-BR": ["Google português do Brasil","pt-PT","pt-BR","pt_PT","pt_BR"], // Portugal Portuguese // Note: in desktop, there's no voice for portugal Portuguese "pt-PT": ["Google português do Brasil","pt-PT","pt_PT"], // Russian "ru-RU": ["Google русский","ru-RU","ru_RU"], // Dutch (holland) "nl-NL": ["Google Nederlands","nl-NL","nl_NL"], // French "fr-FR": ["Google français","fr-FR","fr_FR"], // Polish "pl-PL": ["Google polski","pl-PL","pl_PL"], // Indonesian "id-ID": ["Google Bahasa Indonesia","id-ID","id_ID"], // Hindi "hi-IN": ["Google हिन्दी","hi-IN", "hi_IN"], // Mandarin Chinese "zh-CN": ["Google 普通话(中国大陆)","zh-CN","zh_CN"], // Cantonese Chinese "zh-HK": ["Google 粤語(香港)","zh-HK","zh_HK"], // Native voice "native": ["native"] }; // Important: retrieve the voices of the browser as soon as possible. // Normally, the execution of speechSynthesis.getVoices will return at the first time an empty array. if (window.hasOwnProperty('speechSynthesis')) { speechSynthesis.getVoices(); }else{ console.error("Artyom.js can't speak without the Speech Synthesis API."); } // This instance of webkitSpeechRecognition is the one used by Artyom. if (window.hasOwnProperty('webkitSpeechRecognition')) { this.ArtyomWebkitSpeechRecognition = new (<any>window).webkitSpeechRecognition(); }else{ console.error("Artyom.js can't recognize voice without the Speech Recognition API."); } this.ArtyomProperties = { lang: 'en-GB', recognizing: false, continuous: false, speed: 1, volume: 1, listen: false, mode: "normal", debug: false, helpers: { redirectRecognizedTextOutput: null, remoteProcessorHandler: null, lastSay: null, fatalityPromiseCallback: null }, executionKeyword: null, obeyKeyword: null, speaking: false, obeying: true, soundex: false, name: null }; this.ArtyomGarbageCollection = []; this.ArtyomFlags = { restartRecognition: false }; this.ArtyomGlobalEvents = { ERROR: "ERROR", SPEECH_SYNTHESIS_START: "SPEECH_SYNTHESIS_START", SPEECH_SYNTHESIS_END: "SPEECH_SYNTHESIS_END", TEXT_RECOGNIZED: "TEXT_RECOGNIZED", COMMAND_RECOGNITION_START : "COMMAND_RECOGNITION_START", COMMAND_RECOGNITION_END: "COMMAND_RECOGNITION_END", COMMAND_MATCHED: "COMMAND_MATCHED", NOT_COMMAND_MATCHED: "NOT_COMMAND_MATCHED" }; this.Device = { isMobile: false, isChrome: true }; if( navigator.userAgent.match(/Android/i) || navigator.userAgent.match(/webOS/i) || navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPad/i) || navigator.userAgent.match(/iPod/i) || navigator.userAgent.match(/BlackBerry/i) || navigator.userAgent.match(/Windows Phone/i)){ this.Device.isMobile = true; } if (navigator.userAgent.indexOf("Chrome") == -1) { this.Device.isChrome = false; } /** * The default voice of Artyom in the Desktop. In mobile, you will need to initialize (or force the language) * with a language code in order to find an available voice in the device, otherwise it will use the native voice. */ this.ArtyomVoice = { default: false, lang: "en-GB", localService: false, name: "Google UK English Male", voiceURI: "Google UK English Male" }; } /** * Add dinamically commands to artyom using * You can even add commands while artyom is active. * * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/addcommands * @since 0.6 * @param {Object | Array[Objects]} param * @returns {undefined} */ addCommands(param: any) { let _this = this; let processCommand = (command : ArtyomCommand) => { if(command.hasOwnProperty("indexes")){ _this.ArtyomCommands.push(command); }else{ console.error("The given command doesn't provide any index to execute."); } }; if (param instanceof Array) { for (let i = 0; i < param.length; i++) { processCommand(param[i]); } } else { processCommand(param); } return true; }; /** * The SpeechSynthesisUtterance objects are stored in the artyom_garbage_collector variable * to prevent the wrong behaviour of artyom.say. * Use this method to clear all spoken SpeechSynthesisUtterance unused objects. * * @returns {Array<any>} */ clearGarbageCollection() : Array<any> { return this.ArtyomGarbageCollection = []; }; /** * Displays a message in the console if the artyom propery DEBUG is set to true. * * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/debug * @returns {undefined} */ debug(message: string, type?: string) { let preMessage = `[v${this.getVersion()}] Artyom.js`; if (this.ArtyomProperties.debug === true) { switch (type) { case"error": console.log( `%c${preMessage}:%c ${message}`, 'background: #C12127; color: black;', 'color:black;' ); break; case"warn": console.warn(message); break; case"info": console.log( `%c${preMessage}:%c ${message}`, 'background: #4285F4; color: #FFFFFF', 'color:black;' ); break; default: console.log( `%c${preMessage}:%c ${message}`, 'background: #005454; color: #BFF8F8', 'color:black;' ); break; } } } /** * Artyom have it's own diagnostics. * Run this function in order to detect why artyom is not initialized. * * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/detecterrors * @param {type} callback * @returns {} */ detectErrors() { let _this : Artyom = this; if ((window.location.protocol) == "file:") { let message = "Error: running Artyom directly from a file. The APIs require a different communication protocol like HTTP or HTTPS"; console.error(message); return { code: "artyom_error_localfile", message: message }; } if (!_this.Device.isChrome) { let message = "Error: the Speech Recognition and Speech Synthesis APIs require the Google Chrome Browser to work."; console.error(message); return { code: "artyom_error_browser_unsupported", message: message }; } if (window.location.protocol != "https:") { console.warn( `Warning: artyom is being executed using the '${window.location.protocol}' protocol. The continuous mode requires a secure protocol (HTTPS)` ); } return false; } /** * Removes all the added commands of artyom. * * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/emptycommands * @since 0.6 * @returns {Array} */ emptyCommands() : Array<any> { return this.ArtyomCommands = []; } /** * Returns an object with data of the matched element * * @private * @param {string} comando * @returns {MatchedCommand} */ execute(voz) : MatchedCommand { let _this = this; if (!voz) { console.warn("Internal error: Execution of empty command"); return; } // If artyom was initialized with a name, verify that the name begins with it to allow the execution of commands. if(_this.ArtyomProperties.name){ if(voz.indexOf(_this.ArtyomProperties.name) != 0){ _this.debug(`Artyom requires with a name "${_this.ArtyomProperties.name}" but the name wasn't spoken.`, "warn"); return; } // Remove name from voice command voz = voz.substr(_this.ArtyomProperties.name.length); } _this.debug(">> " + voz); /** @3 * Artyom needs time to think that */ for (let i = 0; i < _this.ArtyomCommands.length; i++) { let instruction = _this.ArtyomCommands[i]; let opciones = instruction.indexes; let encontrado = -1; let wildy = ""; for (let c = 0; c < opciones.length; c++) { let opcion = opciones[c]; if (!instruction.smart) { continue;//Jump if is not smart command } // Process RegExp if(opcion instanceof RegExp){ // If RegExp matches if(opcion.test(voz)){ _this.debug(">> REGEX "+ opcion.toString() + " MATCHED AGAINST " + voz + " WITH INDEX " + c + " IN COMMAND ", "info"); encontrado = parseInt(c.toString()); } // Otherwise just wildcards }else{ if (opcion.indexOf("*") != -1) { ///LOGIC HERE let grupo = opcion.split("*"); if (grupo.length > 2) { console.warn("Artyom found a smart command with " + (grupo.length - 1) + " wildcards. Artyom only support 1 wildcard for each command. Sorry"); continue; } //START SMART COMMAND let before = grupo[0]; let later = grupo[1]; // Wildcard in the end if ((later == "") || (later == " ")) { if ((voz.indexOf(before) != -1) || ((voz.toLowerCase()).indexOf(before.toLowerCase()) != -1)) { wildy = voz.replace(before, ''); wildy = (wildy.toLowerCase()).replace(before.toLowerCase(), ''); encontrado = parseInt(c.toString()); } } else { if ((voz.indexOf(before) != -1) || ((voz.toLowerCase()).indexOf(before.toLowerCase()) != -1)) { if ((voz.indexOf(later) != -1) || ((voz.toLowerCase()).indexOf(later.toLowerCase()) != -1)) { wildy = voz.replace(before, '').replace(later, ''); wildy = (wildy.toLowerCase()).replace(before.toLowerCase(), '').replace(later.toLowerCase(), ''); wildy = (wildy.toLowerCase()).replace(later.toLowerCase(), ''); encontrado = parseInt(c.toString()); } } } } else { console.warn("Founded command marked as SMART but have no wildcard in the indexes, remove the SMART for prevent extensive memory consuming or add the wildcard *"); } } if ((encontrado >= 0)) { encontrado = parseInt(c.toString()); break; } } if (encontrado >= 0) { _this.triggerEvent(_this.ArtyomGlobalEvents.COMMAND_MATCHED); let response : MatchedCommand = { index: encontrado, instruction: instruction, wildcard: { item: wildy, full: voz } }; return response; } }//End @3 /** @1 * Search for IDENTICAL matches in the commands if nothing matches * start with a index match in commands */ for (let i = 0; i < _this.ArtyomCommands.length; i++) { let instruction = _this.ArtyomCommands[i]; let opciones = instruction.indexes; let encontrado = -1; /** * Execution of match with identical commands */ for (let c = 0; c < opciones.length; c++) { let opcion = opciones[c]; if (instruction.smart) { continue;//Jump wildcard commands } if ((voz === opcion)) { _this.debug(">> MATCHED FULL EXACT OPTION " + opcion + " AGAINST " + voz + " WITH INDEX " + c + " IN COMMAND ", "info"); encontrado = parseInt(c.toString()); break; } else if ((voz.toLowerCase() === opcion.toLowerCase())) { _this.debug(">> MATCHED OPTION CHANGING ALL TO LOWERCASE " + opcion + " AGAINST " + voz + " WITH INDEX " + c + " IN COMMAND ", "info"); encontrado = parseInt(c.toString()); break; } } if (encontrado >= 0) { _this.triggerEvent(_this.ArtyomGlobalEvents.COMMAND_MATCHED); let response : MatchedCommand = { index: encontrado, instruction: instruction }; return response; } }//End @1 /** * Step 3 Commands recognition. * If the command is not smart, and any of the commands match exactly then try to find * a command in all the quote. */ for (let i = 0; i < _this.ArtyomCommands.length; i++) { let instruction = _this.ArtyomCommands[i]; let opciones = instruction.indexes; let encontrado = -1; /** * Execution of match with index */ for (let c = 0; c < opciones.length; c++) { if (instruction.smart) { continue;//Jump wildcard commands } let opcion = opciones[c]; if ((voz.indexOf(opcion) >= 0)) { _this.debug(">> MATCHED INDEX EXACT OPTION " + opcion + " AGAINST " + voz + " WITH INDEX " + c + " IN COMMAND ", "info"); encontrado = parseInt(c.toString()); break; } else if (((voz.toLowerCase()).indexOf(opcion.toLowerCase()) >= 0)) { _this.debug(">> MATCHED INDEX OPTION CHANGING ALL TO LOWERCASE " + opcion + " AGAINST " + voz + " WITH INDEX " + c + " IN COMMAND ", "info"); encontrado = parseInt(c.toString()); break; } } if (encontrado >= 0) { _this.triggerEvent(_this.ArtyomGlobalEvents.COMMAND_MATCHED); let response : MatchedCommand = { index: encontrado, instruction: instruction }; return response; } }//End Step 3 /** * If the soundex options is enabled, proceed to process the commands in case that any of the previous * ways of processing (exact, lowercase and command in quote) didn't match anything. * Based on the soundex algorithm match a command if the spoken text is similar to any of the artyom commands. * Example : * If you have a command with "Open Wallmart" and "Open Willmar" is recognized, the open wallmart command will be triggered. * soundex("Open Wallmart") == soundex("Open Willmar") <= true * */ if(_this.ArtyomProperties.soundex){ for (let i = 0; i < _this.ArtyomCommands.length; i++) { let instruction : ArtyomCommand = _this.ArtyomCommands[i]; let opciones = instruction.indexes; let encontrado = -1; for (let c = 0; c < opciones.length; c++) { let opcion = opciones[c]; if (instruction.smart) { continue;//Jump wildcard commands } if(_this.soundex(voz) == _this.soundex(opcion)){ _this.debug( `>> Matched Soundex command '${opcion}' AGAINST '${voz}' with index ${c}`, "info" ); encontrado = parseInt(c.toString()); _this.triggerEvent(_this.ArtyomGlobalEvents.COMMAND_MATCHED); let response : MatchedCommand = { index: encontrado, instruction: instruction }; return response; } } } } _this.debug(`Event reached : ${_this.ArtyomGlobalEvents.NOT_COMMAND_MATCHED}`); _this.triggerEvent(_this.ArtyomGlobalEvents.NOT_COMMAND_MATCHED); return; } /** * Force artyom to stop listen even if is in continuos mode. * * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/fatality * @returns {Boolean} */ fatality() { let _this : Artyom = this; //fatalityPromiseCallback return new Promise((resolve,reject) => { // Expose the fatality promise callback to the helpers object of Artyom. // The promise isn't resolved here itself but in the onend callback of // the speechRecognition instance of artyom _this.ArtyomProperties.helpers.fatalityPromiseCallback = resolve; try{ // If config is continuous mode, deactivate anyway. _this.ArtyomFlags.restartRecognition = false; _this.ArtyomWebkitSpeechRecognition.stop(); }catch(e){ reject(e); } }); } /** * Returns an array with all the available commands for artyom. * * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/getavailablecommands * @readonly * @returns {Array} */ getAvailableCommands() { return this.ArtyomCommands; } /** * Artyom can return inmediately the voices available in your browser. * * @readonly * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/getvoices * @returns {Array} */ getVoices() { return window.speechSynthesis.getVoices(); } /** * Verify if the browser supports speechSynthesis. * * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/speechsupported * @returns {Boolean} */ speechSupported() { return 'speechSynthesis' in window; } /** * Verify if the browser supports webkitSpeechRecognition. * * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/recognizingsupported * @returns {Boolean} */ recognizingSupported() { return 'webkitSpeechRecognition' in window; } /** * Stops the actual and pendings messages that artyom have to say. * * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/shutup * @returns {undefined} */ shutUp() { if ('speechSynthesis' in window) { do { window.speechSynthesis.cancel(); } while (window.speechSynthesis.pending === true); } this.ArtyomProperties.speaking = false; this.clearGarbageCollection(); } /** * Returns an object with the actual properties of artyom. * * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/getproperties * @returns {object} */ getProperties() { return this.ArtyomProperties; } /** * Returns the code language of artyom according to initialize function. * if initialize not used returns english GB. * * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/getlanguage * @returns {String} */ getLanguage() { return this.ArtyomProperties.lang; } /** * Retrieves the used version of Artyom.js * * @returns {String} */ getVersion() { return '1.0.6'; } /** * Artyom awaits for orders when this function * is executed. * * If artyom gets a first parameter the instance will be stopped. * * @private * @returns {undefined} */ hey(resolve: Function, reject: Function) { let start_timestamp; let artyom_is_allowed; let _this : Artyom = this; /** * On mobile devices the recognized text is always thrown twice. * By setting the following configuration, fixes the issue */ if(this.Device.isMobile){ this.ArtyomWebkitSpeechRecognition.continuous = false; this.ArtyomWebkitSpeechRecognition.interimResults = false; this.ArtyomWebkitSpeechRecognition.maxAlternatives = 1; }else{ this.ArtyomWebkitSpeechRecognition.continuous = true; this.ArtyomWebkitSpeechRecognition.interimResults = true; } this.ArtyomWebkitSpeechRecognition.lang = this.ArtyomProperties.lang; this.ArtyomWebkitSpeechRecognition.onstart = () => { _this.debug("Event reached : " + _this.ArtyomGlobalEvents.COMMAND_RECOGNITION_START); _this.triggerEvent(_this.ArtyomGlobalEvents.COMMAND_RECOGNITION_START); _this.ArtyomProperties.recognizing = true; artyom_is_allowed = true; resolve(); }; /** * Handle all artyom posible exceptions * * @param {type} event * @returns {undefined} */ this.ArtyomWebkitSpeechRecognition.onerror = (event) => { // Reject promise on initialization reject(event.error); // Dispath error globally (artyom.when) _this.triggerEvent(_this.ArtyomGlobalEvents.ERROR,{ code: event.error }); if (event.error == 'audio-capture') { artyom_is_allowed = false; } if (event.error == 'not-allowed') { artyom_is_allowed = false; if (event.timeStamp - start_timestamp < 100) { _this.triggerEvent(_this.ArtyomGlobalEvents.ERROR, { code: "info-blocked", message: "Artyom needs the permision of the microphone, is blocked." }); } else { _this.triggerEvent(_this.ArtyomGlobalEvents.ERROR, { code: "info-denied", message: "Artyom needs the permision of the microphone, is denied" }); } } }; /** * Check if continuous mode is active and restart the recognition. * Throw events too. * * @returns {undefined} */ _this.ArtyomWebkitSpeechRecognition.onend = function () { if (_this.ArtyomFlags.restartRecognition === true) { if (artyom_is_allowed === true) { _this.ArtyomWebkitSpeechRecognition.start(); _this.debug("Continuous mode enabled, restarting", "info"); } else { console.error("Verify the microphone and check for the table of errors in sdkcarlos.github.io/sites/artyom.html to solve your problem. If you want to give your user a message when an error appears add an artyom listener"); } _this.triggerEvent(_this.ArtyomGlobalEvents.COMMAND_RECOGNITION_END,{ code: "continuous_mode_enabled", message: "OnEnd event reached with continuous mode" }); }else{ // If the fatality promise callback was set, invoke it if(_this.ArtyomProperties.helpers.fatalityPromiseCallback){ // As the speech recognition doesn't finish really, wait 500ms // to trigger the real fatality callback setTimeout(() => { _this.ArtyomProperties.helpers.fatalityPromiseCallback(); }, 500); _this.triggerEvent(_this.ArtyomGlobalEvents.COMMAND_RECOGNITION_END,{ code: "continuous_mode_disabled", message: "OnEnd event reached without continuous mode" }); } } _this.ArtyomProperties.recognizing = false; }; /** * Declare the processor dinamycally according to the mode of artyom * to increase the performance. * * @type {Function} * @return */ let onResultProcessor; // Process the recognition in normal mode if(_this.ArtyomProperties.mode == "normal"){ onResultProcessor = (event) => { if (!_this.ArtyomCommands.length) { _this.debug("No commands to process in normal mode."); return; } let cantidadResultados = event.results.length; _this.triggerEvent(_this.ArtyomGlobalEvents.TEXT_RECOGNIZED); for (let i = event.resultIndex; i < cantidadResultados; ++i) { let identificated = event.results[i][0].transcript; if (event.results[i].isFinal) { let comando : MatchedCommand = _this.execute(identificated.trim()); // Redirect the output of the text if necessary if (typeof (_this.ArtyomProperties.helpers.redirectRecognizedTextOutput) === "function") { _this.ArtyomProperties.helpers.redirectRecognizedTextOutput(identificated, true); } if ((comando) && (_this.ArtyomProperties.recognizing == true)) { _this.debug("<< Executing Matching Recognition in normal mode >>", "info"); _this.ArtyomWebkitSpeechRecognition.stop(); _this.ArtyomProperties.recognizing = false; // Execute the command if smart if (comando.wildcard) { comando.instruction.action(comando.index, comando.wildcard.item, comando.wildcard.full); // Execute a normal command } else { comando.instruction.action(comando.index); } break; } } else { // Redirect output when necesary if (typeof (_this.ArtyomProperties.helpers.redirectRecognizedTextOutput) === "function") { _this.ArtyomProperties.helpers.redirectRecognizedTextOutput(identificated, false); } if (typeof (_this.ArtyomProperties.executionKeyword) === "string") { if (identificated.indexOf(_this.ArtyomProperties.executionKeyword) != -1) { let comando = _this.execute(identificated.replace(_this.ArtyomProperties.executionKeyword, '').trim()); if ((comando) && (_this.ArtyomProperties.recognizing == true)) { _this.debug("<< Executing command ordered by ExecutionKeyword >>", 'info'); _this.ArtyomWebkitSpeechRecognition.stop(); _this.ArtyomProperties.recognizing = false; //Executing Command Action if (comando.wildcard) { comando.instruction.action(comando.index, comando.wildcard.item, comando.wildcard.full); } else { comando.instruction.action(comando.index); } break; } } } _this.debug("Normal mode : " + identificated); } } } } // Process the recognition in quick mode if(_this.ArtyomProperties.mode == "quick"){ onResultProcessor = (event) => { if (!_this.ArtyomCommands.length) { _this.debug("No commands to process."); return; } let cantidadResultados = event.results.length; _this.triggerEvent(_this.ArtyomGlobalEvents.TEXT_RECOGNIZED); for (let i = event.resultIndex; i < cantidadResultados; ++i) { let identificated = event.results[i][0].transcript; if (!event.results[i].isFinal) { let comando : MatchedCommand = _this.execute(identificated.trim()); //Redirect output when necesary if (typeof (_this.ArtyomProperties.helpers.redirectRecognizedTextOutput) === "function") { _this.ArtyomProperties.helpers.redirectRecognizedTextOutput(identificated, true); } if ((comando) && (_this.ArtyomProperties.recognizing == true)) { _this.debug("<< Executing Matching Recognition in quick mode >>", "info"); _this.ArtyomWebkitSpeechRecognition.stop(); _this.ArtyomProperties.recognizing = false; //Executing Command Action if (comando.wildcard) { comando.instruction.action(comando.index, comando.wildcard.item); } else { comando.instruction.action(comando.index); } break; } } else { let comando : MatchedCommand = _this.execute(identificated.trim()); //Redirect output when necesary if (typeof (_this.ArtyomProperties.helpers.redirectRecognizedTextOutput) === "function") { _this.ArtyomProperties.helpers.redirectRecognizedTextOutput(identificated, false); } if ((comando) && (_this.ArtyomProperties.recognizing == true)) { _this.debug("<< Executing Matching Recognition in quick mode >>", "info"); _this.ArtyomWebkitSpeechRecognition.stop(); _this.ArtyomProperties.recognizing = false; //Executing Command Action if (comando.wildcard) { comando.instruction.action(comando.index, comando.wildcard.item); } else { comando.instruction.action(comando.index); } break; } } _this.debug("Quick mode : " + identificated); } } } // Process the recognition in remote mode if(_this.ArtyomProperties.mode == "remote"){ onResultProcessor = (event) => { let cantidadResultados = event.results.length; _this.triggerEvent(_this.ArtyomGlobalEvents.TEXT_RECOGNIZED); if (typeof (_this.ArtyomProperties.helpers.remoteProcessorHandler) !== "function") { return _this.debug("The remoteProcessorService is undefined.","warn"); } for (let i = event.resultIndex; i < cantidadResultados; ++i) { let identificated = event.results[i][0].transcript; _this.ArtyomProperties.helpers.remoteProcessorHandler({ text: identificated, isFinal:event.results[i].isFinal }); } } } /** * Process the recognition event with the previously * declared processor function. * * @param {type} event * @returns {undefined} */ _this.ArtyomWebkitSpeechRecognition.onresult = (event) => { if(_this.ArtyomProperties.obeying){ onResultProcessor(event); }else{ // Handle obeyKeyword if exists and artyom is not obeying if(!_this.ArtyomProperties.obeyKeyword){ return; } let temporal = ""; let interim = ""; for (let i = 0; i < event.results.length; ++i) { if (event.results[i].isFinal) { temporal += event.results[i][0].transcript; } else { interim += event.results[i][0].transcript; } } _this.debug("Artyom is not obeying","warn"); // If the obeyKeyword is found in the recognized text // enable command recognition again if(((interim).indexOf(_this.ArtyomProperties.obeyKeyword) > -1) || (temporal).indexOf(_this.ArtyomProperties.obeyKeyword) > -1){ _this.ArtyomProperties.obeying = true; } } }; if (_this.ArtyomProperties.recognizing) { _this.ArtyomWebkitSpeechRecognition.stop(); _this.debug("Event reached : " + _this.ArtyomGlobalEvents.COMMAND_RECOGNITION_END); _this.triggerEvent(_this.ArtyomGlobalEvents.COMMAND_RECOGNITION_END); } else { try { _this.ArtyomWebkitSpeechRecognition.start(); } catch (e) { _this.triggerEvent(_this.ArtyomGlobalEvents.ERROR,{ code: "recognition_overlap", message: "A webkitSpeechRecognition instance has been started while there's already running. Is recommendable to restart the Browser" }); } } } /** * Set up artyom for the application. * * This function will set the default language used by artyom * or notice the user if artyom is not supported in the actual * browser * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/initialize * @param {Object} config * @returns {Boolean} */ initialize(config: ArtyomProperties) : Promise<boolean> { let _this = this; if (typeof (config) !== "object") { return Promise.reject("You must give the configuration for start artyom properly."); } if (config.hasOwnProperty("lang")) { _this.ArtyomVoice = _this.getVoice(config.lang); _this.ArtyomProperties.lang = config.lang; } if (config.hasOwnProperty("continuous")) { if (config.continuous) { this.ArtyomProperties.continuous = true; this.ArtyomFlags.restartRecognition = true; } else { this.ArtyomProperties.continuous = false; this.ArtyomFlags.restartRecognition = false; } } if (config.hasOwnProperty("speed")) { this.ArtyomProperties.speed = config.speed; } if (config.hasOwnProperty("soundex")) { this.ArtyomProperties.soundex = config.soundex; } if (config.hasOwnProperty("executionKeyword")) { this.ArtyomProperties.executionKeyword = config.executionKeyword; } if (config.hasOwnProperty("obeyKeyword")) { this.ArtyomProperties.obeyKeyword = config.obeyKeyword; } if (config.hasOwnProperty("volume")) { this.ArtyomProperties.volume = config.volume; } if(config.hasOwnProperty("listen")){ this.ArtyomProperties.listen = config.listen; } if(config.hasOwnProperty("name")){ this.ArtyomProperties.name = config.name; } if(config.hasOwnProperty("debug")){ this.ArtyomProperties.debug = config.debug; }else{ console.warn("The initialization doesn't provide how the debug mode should be handled. Is recommendable to set this value either to true or false."); } if (config.mode) { this.ArtyomProperties.mode = config.mode; } if (this.ArtyomProperties.listen === true) { return new Promise((resolve,reject) => { _this.hey(resolve , reject); }); } return Promise.resolve(true); } /** * Add commands like an artisan. If you use artyom for simple tasks * then probably you don't like to write a lot to achieve it. * * Use the artisan syntax to write less, but with the same accuracy. * * @disclaimer Not a promise-based implementation, just syntax. * @returns {Boolean} */ on(indexes: Array<any>, smart?: Boolean) { let _this = this; return { then: (action: Function ) => { let command : ArtyomCommand = { indexes:indexes, action: action }; if(smart){ command.smart = true; } _this.addCommands(command); } }; } /** * Generates an artyom event with the designed name * * @param {type} name * @returns {undefined} */ triggerEvent(name: string, param?: any) { let event = new CustomEvent( name, { 'detail': param }); document.dispatchEvent(event); return event; } /** * Repeats the last sentence that artyom said. * Useful in noisy environments. * * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/repeatlastsay * @param {Boolean} returnObject If set to true, an object with the text and the timestamp when was executed will be returned. * @returns {Object} */ repeatLastSay(returnObject?: Boolean) { let last = this.ArtyomProperties.helpers.lastSay; if (returnObject) { return last; } else { if (last != null) { this.say(last.text); } } } /** * Create a listener when an artyom action is called. * * @tutorial http://docs.ourcodeworld.com/projects/artyom-js/documentation/methods/when * @param {type} event * @param {type} action * @returns {undefined} */ when(event : string, action: Function) { return document.addEventListener(event, (e) => { action(e["detail"]); }, false); } /** * Process the recognized text if artyom is active in remote mode. * * @returns {Boolean} */ remoteProcessorService(action: Function) { this.ArtyomProperties.helpers.remoteProcessorHandler = action; return true; } /** * Verify if there's a voice available for a language using its language code identifier. * * @return {Boolean} */ voiceAvailable(languageCode: string) { return typeof(this.getVoice(languageCode)) !== "undefined"; } /** * A boolean to check if artyom is obeying commands or not. * * @returns {Boolean} */ isObeying() { return this.ArtyomProperties.obeying; } /** * Allow artyom to obey commands again. * * @returns {Boolean} */ obey() { return this.ArtyomProperties.obeying = true; } /** * Pause the processing of commands. Artyom still listening in the background and it can be resumed after a couple of seconds. * * @returns {Boolean} */ dontObey() { return this.ArtyomProperties.obeying = false; } /** * This function returns a boolean according to the speechSynthesis status * if artyom is speaking, will return true. * * Note: This is not a feature of speechSynthesis, therefore this value hangs on * the fiability of the onStart and onEnd events of the speechSynthesis * * @since 0.9.3 * @summary Returns true if speechSynthesis is active * @returns {Boolean} */ isSpeaking() { return this.ArtyomProperties.speaking; } /** * This function returns a boolean according to the SpeechRecognition status * if artyom is listening, will return true. * * Note: This is not a feature of SpeechRecognition, therefore this value hangs on * the fiability of the onStart and onEnd events of the SpeechRecognition * * @since 0.9.3 * @summary Returns true if SpeechRecognition is active * @returns {Boolean} */ isRecognizing() { return this.ArtyomProperties.recognizing; } /** * This function will return the webkitSpeechRecognition object used by artyom * retrieve it only to debug on it or get some values, do not make changes directly * * @readonly * @since 0.9.2 * @summary Retrieve the native webkitSpeechRecognition object * @returns {Object webkitSpeechRecognition} */ getNativeApi() { return this.ArtyomWebkitSpeechRecognition; } /** * Returns the SpeechSynthesisUtterance garbageobjects. * * @returns {Array} */ getGarbageCollection() { return this.ArtyomGarbageCollection; } /** * Retrieve a single voice of the browser by it's language code. * It will return the first voice available for the language on every device. * * @param languageCode */ getVoice(languageCode: string) { let voiceIdentifiersArray = this.ArtyomVoicesIdentifiers[languageCode]; if(!voiceIdentifiersArray){ console.warn(`The providen language ${languageCode} isn't available, using English Great britain as default` ); voiceIdentifiersArray = this.ArtyomVoicesIdentifiers["en-GB"]; } let voice = undefined; let voices = speechSynthesis.getVoices(); let voicesLength = voiceIdentifiersArray.length; for(let i = 0;i < voicesLength; i++){ let foundVoice = voices.filter( (voice) => { return ( (voice.name == voiceIdentifiersArray[i]) || (voice.lang == voiceIdentifiersArray[i]) ); })[0]; if(foundVoice){ voice = foundVoice; break; } } return voice; } /** * Artyom provide an easy way to create a * dictation for your user. * * Just create an instance and start and stop when you want * * @returns Object | newDictation */ newDictation(settings) { let _this : Artyom = this; if (!_this.recognizingSupported()) { console.error("SpeechRecognition is not supported in this browser"); return false; } let dictado = new (<any>window).webkitSpeechRecognition(); dictado.continuous = true; dictado.interimResults = true; dictado.lang = _this.ArtyomProperties.lang; dictado.onresult = function (event) { let temporal = ""; let interim = ""; for (let i = 0; i < event.results.length; ++i) { if (event.results[i].isFinal) { temporal += event.results[i][0].transcript; } else { interim += event.results[i][0].transcript; } } if (settings.