Source: managers/speech-manager.js

/**
 * @module managers/speech-manager
 * @description This module manages speech
 * @requires modules/hash
 * @requires common/view
 * @requires managers/template-manager
 * @requires managers/prsonas-manager
 * @requires jHammer
 */
 

define([
    "managers/prsonas-manager", 
    "managers/timer-manager"
], 

function (Prsonas, Timer) {

    // ---------------------------------------------------------------
    //
    // SPEECH MANAGER
    //
    // ---------------------------------------------------------------

    var Speech = function () {
        console.log(" * <speech>");

        this.oPrsonas = Prsonas;
        this.oTimer   = Timer;

        window.tEvent.eventStr.EVENT_IN_WALKAWAY = "EVENT_IN_WALKAWAY";   
        window.tEvent.eventStr.EVENT_IN_WALKUP   = "EVENT_IN_WALKUP";   

        this.ui = {
            speakPanel            : ".js-complex-speak",        
            speakCopy             : ".js-complex-speak-copy",
            ccPanel               : ".js-complex-cc",
            ccCopy                : ".js-complex-cc-copy",
            micToggle             : ".js-complex-mic",
            micIcon               : ".js-complex-mic-icon",
            micSwitch             : ".js-complex-mic-switch",
            backBtn               : ".js-complex-back-btn",
            homeBtn               : ".js-complex-home-btn",
            aslFadeContent        : ".js-complex-asl-fade"
        };

        this.def = {
            CLASS_HIDE_ME      : "hide-me",
            SENTIMENT_POSITIVE : "_positive",
            SENTIMENT_NEGATIVE : "_negative"
        };

        // forced mic toggle
        this.micEnabled = true;

        // json config
        this.jConfig = -1;  

        // manifest 
        this.manifest = -1;

        // enable first load events
        this.firstLoad = true;

        this.qaUpdateDate = -1;

        this.init();
    };

    Speech.prototype = {    
       
        // ______________________________________________________________
        //                                                           init
        /**
            * init
            * @description Initializes class
            * @memberOf module:managers/speech-manager
            * @instance
        */
        init: function() {   
            var self = this;

            if (typeof window.oModel === "undefined" || window.oModel.modelReady === false){
                window.tEvent.addListener("EVENT_MODEL_READY", function(evt, data) {
                    self.onModelReady();                  
                });                
            } else {
                self.onModelReady();   
            }        
        },


        // ______________________________________________________________
        //                                                     initSpeech
        /** 
        * initSpeech
        * @description Init speech 
        * @fires assignListeners();
        * @memberOf module:managers/speech-manager
        * @instance
        */
        initSpeech: function() {           
            var self = this;

            if (!this.jConfig.speech.showMicToggle){
                $(this.ui.micToggle).addClass(this.def.CLASS_HIDE_ME);
            }

            // set this.qaUpdateDate
            if (typeof window.oPrsonas.avatar !== "undefined" && typeof window.oPrsonas.avatar.getQAProjectInfo !== "undefined"){
                this.qaUpdateDate = window.oPrsonas.avatar.getQAProjectInfo().data.lastDeployedDateTime;
            }

            this.assignListeners();
        },

        // ______________________________________________________________
        //                                                assignListeners
        /** 
        * assignListeners
        * @description Assigns listeners to events used in this class.
        * @listens  EVENT_ADMIN_GOT_MANIFEST
        * @listens  EVENT_TEMPLATE_LOADED
        * @listens  EVENT_TIMER_EXPIRED
        * @listens  EVENT_HISTORY
        * @listens  avatar.eventTypes["CC"]
        * @listens  avatar.eventTypes["END_CC"]
        * @listens  avatar.eventTypes["PHRASE_RECOGNIZED"]
        * @memberOf module:managers/speech-manager
        * @instance
        */
        assignListeners: function() {  

            var self = this;

            window.tEvent.addListener("EVENT_ADMIN_GOT_MANIFEST", function(evt, data){                
                self.manifest = data;
            });

            window.tEvent.addListener(window.tEvent.eventStr.EVENT_TEMPLATE_LOADED, function(evt, eventId){ 
                self.onPageLoad(eventId);                
            });

            window.tEvent.addListener("EVENT_TIMER_EXPIRED", function(evt, data){                
                self.doWalkAway();
            });

            window.tEvent.addListener([this.jConfig.prsonas.timers.expiredEvent], function(evt, data){ 
                self.doWalkAway();
            });

            $("body").hammer().on("click", this.ui.micSwitch, function(e){                  
                self.onToggleMic();
            });  

            // cc event
            this.oPrsonas.sdk.subscribe(this.oPrsonas.avatar.eventTypes["CC"], function(evt) {
                self.onCcEvent(evt);
            });

            // endcc event
            this.oPrsonas.sdk.subscribe(this.oPrsonas.avatar.eventTypes["END_CC"], function(evt) {
                self.onEndCcEvent(evt);
            });

            // PHRASE_RECOGNIZED event
            this.oPrsonas.sdk.subscribe(this.oPrsonas.avatar.eventTypes["PHRASE_RECOGNIZED"], function(evt) {
                self.onSpeechPhraseRecognized(evt)
            });


            // timer expired
            var walkUpevent = "";
            for (var key in this.jConfig.receiveKeyEvents) {
                if (key.toLowerCase() === "walkaway"){
                    walkUpevent = this.jConfig.receiveKeyEvents[key].event;
                    break;
                }
            }
            window.tEvent.addListener(["EVENT_TIMER_EXPIRED", walkUpevent], function(evt, data){ 
                self.onQaUpdateCheck();
            });

        },


        // --------------------------------------------------------------
        // HELPERS
        // --------------------------------------------------------------
      
        // ______________________________________________________________
        //                                                   toggleSpeech
        /** 
            * toggleSpeech
            * @description Enables or Disables speech recognition
            * @param {('enable'|'disable')} state - toggle to state
            * @memberOf module:managers/speech-manager
            * @instance
        */
        toggleSpeech: function(state) {   

            var $micToggle  = $(this.ui.micToggle);
            var lang        = $("body").attr("data-lang");
            var langId      = this.jConfig.language.nuBrowserIds[lang];

            if (!this.micEnabled){
                state = "disable";
                $micToggle.removeClass("is-enabled");
                $micToggle.addClass("is-disabled");
            } else {
                $micToggle.removeClass("is-disabled");
                $micToggle.addClass("is-enabled");
            }

            switch(state){
                case "disable":
                    $(this.ui.speakIcon).removeClass("is-listening");
                    this.oPrsonas.avatar.stopListening();
                    if (this.micEnabled){
                        this.oTimer.killTimer();
                    } 
                    this.onIndicatorUpdate({
                        id: "microphone",
                        status: false
                    });  
                    break;

                case "enable":
                    $(this.ui.speakIcon).addClass("is-listening");
                    
                    if (typeof langId !== "undefined"){
                        this.oPrsonas.avatar.startListening(langId);
                    }                    

                    // trigger listening intent
                    if (this.jConfig.speech.listeningIntent !== ""){

                        var doListen = true;
                        for (let i = 0; i < this.jConfig.speech.listeningIntentExceptionModules.length; i++){
                            if ( $("body").attr("data-module-id") === this.jConfig.speech.listeningIntentExceptionModules[i]){
                                doListen = false;
                            }
                        }

                        if (doListen) {
                            window.oPrsonas.sendKey(this.jConfig.speech.listeningIntent);
                        }
                        
                    }

                    this.oTimer.resetTimeout();
                    window.tEvent.fire(window.tEvent.eventStr.EVENT_IN_WALKUP);
                    this.onIndicatorUpdate({
                        id: "microphone",
                        status: true
                    });              
                    break;
            }
            

        },

        // ______________________________________________________________
        //                                                     doWalkAway
        /**
            * doWalkAway
            * @description Sets the WalkAway state
            * @fires EVENT_IN_WALKAWAY
            * @memberOf module:managers/speech-manager
            * @instance
        */
        doWalkAway: function() {      
            this.resetSpeech(); 

            window.tEvent.fire(window.tEvent.eventStr.EVENT_LANGUAGE_RESET);

            this.toggleSpeech("disable");

            if (!this.micEnabled) {
                this.onToggleMic();
            }
            
            window.tEvent.fire(window.tEvent.eventStr.EVENT_IN_WALKAWAY);
        },

        // ______________________________________________________________
        //                                                   resetSpeech
        /**
            * resetSpeech
            * @description Resets the closed caption and speech panels
            * @param {Boolean} [isFromEndCc=false] If true that this call is coming from an endCC event.
            * @memberOf module:managers/speech-manager
            * @instance
        */
        resetSpeech: function(isFromEndCc) {  
            var self = this;
            var ccFadeDelay = this.jConfig.speech.ccPanel.animation.delayFadoutOnEndCc;
                        
            if (typeof isFromEndCc === "undefined" || !isFromEndCc){
                ccFadeDelay = 0;
            }

            // kill tweens 
            TweenLite.killTweensOf($(this.ui.aslFadeContent));
            TweenLite.killTweensOf($(this.ui.ccPanel));
            TweenLite.killTweensOf($(this.ui.speakPanel));

            // fade in asl fade content
            TweenLite.to($(this.ui.aslFadeContent), 1, {opacity: 1}); 

            // clear cc copy
            TweenLite.to($(this.ui.ccPanel), 1, {opacity: 0, delay: ccFadeDelay, onComplete:function(){
                $(self.ui.ccCopy).scrollTop(0);
                $(self.ui.ccCopy).html("");
                $(self.ui.ccPanel).css("display", "none");                
            }});

            // clear speak copy
            TweenLite.to($(this.ui.speakPanel), 1, {opacity: 0, onComplete:function(){
                $(self.ui.speakPanel).css("display", "none");
                $(self.ui.speakCopy).html("");
            }});
        },


        // ______________________________________________________________
        //                                                         showCC
        /**
            * showCC
            * @description Shows the closed caption copy of the ID found in content/json/langCopy.json 
            * @param  {String} data - data of the closed caption 
            * @memberOf module:managers/speech-manager
            * @instance
        */
        showCC: function(data) { 
            var ccData = {};
            var lang = $("body").attr("data-lang");
            var ccConfig;

            if (typeof window.oLang.jLangConfig.ccCopy[lang] !== "undefined"){
                ccConfig = window.oLang.jLangConfig.ccCopy[lang];
            }

            
            var animConfig = this.jConfig.speech.ccPanel.animation;
            var isFound = false;
            var temp;                

            // update speech
            this.onIndicatorUpdate({
                id: "speech"
            });  

            if (!this.jConfig.services.speechToText.alwaysListen){
                this.toggleSpeech("disable");
            }            
            this.oTimer.killTimer();  

            // get cc config.
            if (typeof ccConfig !== "undefined"){
                for (var key in ccConfig){           
                    if (key.toLowerCase() === data.value.toLowerCase()){

                        // proxy?
                        if (typeof ccConfig[key].copyProxy !== "undefined" && ccConfig[key].copyProxy !==""){
                            ccData = ccConfig[ccConfig[key].copyProxy];
                        } else {
                            ccData = ccConfig[key];
                        }
                        isFound = true;
                        break;
                    }
                }
            } 

            if (!isFound){
                ccData = {
                    "copy"        : data.properties.copy
                };
            }
            ccData.duration = Number(data.properties.duration);

            // fade out asl fade content
            if (lang.toLowerCase() === "_asl" || lang.toLowerCase() === "_bsl"){
               TweenLite.to($(this.ui.aslFadeContent), 1, {opacity: 0.17});  
           }

            // show and animate spoken panel0
            TweenLite.killTweensOf($(this.ui.ccPanel));
            TweenLite.killTweensOf($(this.ui.ccCopy));
            $(this.ui.ccCopy).html(ccData.copy + "<br>&nbsp;");
            $(this.ui.ccCopy).scrollTop(0);

            
            
            $(this.ui.ccPanel).css("display", "block");
            $(this.ui.ccPanel).css("opacity", "0");
            $(this.ui.ccPanel).css("marginTop", "-35px");
            TweenLite.to($(this.ui.ccPanel), .5, {opacity: 1, marginTop: "0px"});   

            // do scroll  
            const elHeight = $(this.ui.ccCopy).height();
            const elActualHeight = $(this.ui.ccCopy)[0].scrollHeight;


            // init nice scroller
            $(this.ui.ccCopy).niceScroll({emulatetouch:true, touchbehavior:true, autohidemode:true}); 


            const scrollAmount = elActualHeight - elHeight; 
            const adjustedDuration =  ccData.duration - (ccData.duration * 0.3); // adds buffer

            const visibleFraction = Math.min(elHeight / elActualHeight, 1.0);
            const pauseDuration = adjustedDuration * visibleFraction; // in seconds
            const scrollDuration = ccData.duration - pauseDuration; // in seconds
            const scrollDistance = elActualHeight - elHeight; // pixels to scroll
            const scrollSpeed = scrollDuration > 0 ? scrollDistance / scrollDuration : 0; // pixels per second

            if (scrollDuration > 0) {
                TweenLite.to($(this.ui.ccCopy), scrollDuration, {scrollTop: scrollDistance, ease: "none"}).delay(pauseDuration);
            }

        },


        // ______________________________________________________________
        //                                                  isValidIntent
        /** 
        * isValidIntent
        * @description Validates intent against  manifest.IntentMetaData
        * @param  {String} intentStr - Intent to validate
        * @return {Object} Returns the IntentMetaData object for that intent
        * @memberOf module:managers/speech-manager
        * @instance
        */
        isValidIntent: function(intentStr) { 
            var returnObj = window.oModel.getReturnObj();
            returnObj.status = "error";
            for (var key in this.manifest.IntentMetaData) {
                if (key.toLowerCase() === intentStr){
                    returnObj.status = "ok";
                    returnObj.data = this.manifest.IntentMetaData[key];
                    break;
                }
            }

            return(returnObj);
        },


        // ______________________________________________________________
        //                                             triggerActionEvent
        /**
            * triggerActionEvent
            * @description Triggers an action event from processed phrase metadata results
            * @param {Object} phraseObj - Phrase object from speech to text.
            * @fires Event associated with action
            * @memberOf module:managers/speech-manager
            * @instance
        */        
        triggerActionEvent: function(phraseObj) { 
            var actionsConfig = this.jConfig.services.qaSpokenActions;
            var lang = $("body").attr("data-lang").replace("_", "").toLowerCase();

            var intent = "";

            // speech
            if (typeof phraseObj._sendKeyDone !== "undefined" && !phraseObj._sendKeyDone){
                if (typeof phraseObj.action_speech === "undefined" && typeof phraseObj.action_ux !== "undefined" && phraseObj.action_ux.context !== "page") {
                    if (typeof phraseObj._raw.properties.answer.qnaId !== "undefined") {
                        intent = this.formatQnaIntent(phraseObj._raw.properties.answer.qnaId);
                    } else {
                        intent = this.formatIntentName(phraseObj._raw.value);
                    }
                    if (phraseObj.answer !== "") {
                        window.oPrsonas.sendKey(intent, phraseObj.answer);
                        phraseObj._sendKeyDone = true;
                    }
                    
                } else if (typeof phraseObj.action_speech !== "undefined") {
                    if (phraseObj.action_speech.context === lang){
                        window.oPrsonas.sendKey(phraseObj.action_speech.id);
                        phraseObj._sendKeyDone = true;
                    } else {
                        if (typeof phraseObj._raw.properties.answer.qnaId !== "undefined") {
                            intent = this.formatQnaIntent(phraseObj._raw.properties.answer.qnaId);
                        } else {
                            intent = this.formatIntentName(phraseObj._raw.value);
                        }
                        if (phraseObj.answer !== "") {
                            window.oPrsonas.sendKey(intent, phraseObj.answer);
                            phraseObj._sendKeyDone = true;
                        }
                    }
                }  
            }


            // action_ux handling
            if (typeof phraseObj.action_ux !== "undefined"){
                if (typeof actionsConfig[phraseObj.action_ux.context] !== "undefined"){

                    if (typeof actionsConfig[phraseObj.action_ux.context][phraseObj.action_ux.id] !== "undefined"){
                        window.tEvent.fire(actionsConfig[phraseObj.action_ux.context][phraseObj.action_ux.id], phraseObj);
                    }

                }
                
            }
           

        },
        // ______________________________________________________________
        //                                               formatIntentName
        /**
            * formatIntentName
            * @description Reformats string for consistant valid intent name used for dynamic speech.
            * @param {String} intentStr - String to be formated
            * @returns {String} Formatted string
            * @memberOf module:managers/speech-manager
            * @instance
        */  
        formatIntentName: function(intentStr) { 

            intentStr = window.helpers.removeSpecialChars(intentStr).trim();
            intentStr = intentStr.split(" ").join("_");

            return( intentStr.toLowerCase() );
        },


        // ______________________________________________________________
        //                                               formatQnaIntent
        /**
            * formatQnaIntent
            * @description Reformats qna ID to be used as intent.
            * @param {String} qnaID - String to be formated
            * @returns {String} Formatted string
            * @memberOf module:managers/speech-manager
            * @instance
        */  
        formatQnaIntent: function(qnaID) { 
            
            intentStr = "qna_" + String(qnaID);

            return(intentStr);
        },


        // ______________________________________________________________
        //                                           processSpeechContext
        /**
            * processSpeechContext
            * @description Process the context of the speech to text object. Context can be comment, navigation, location. Options include comment_intent, navigation_back, navigation_home, navigation_intent, location_locationID
            * @param {Object} phraseObj - Phrase object from speech to text.
            * @fires triggerActionEvent()
            * @memberOf module:managers/speech-manager
            * @instance
        */  
        processSpeechContext: function(phraseObj) { 

            if (typeof this.jConfig.services.qaSpokenActions === "undefined"){
                console.warn(" ! <iha.processSpeechContext> jConfig.services.qaSpokenActions is not defined");
            }

            var actionsConfig = this.jConfig.services.qaSpokenActions;
            var lang = $("body").attr("data-lang");
            var intent = "";
            phraseObj._sendKeyDone = false;

            // speech handling
            if ( (typeof phraseObj.action_speech === "undefined" || phraseObj.action_speech === "genai") && (typeof phraseObj.action_ux === "undefined") ){

                var doFallback = false;
                if (typeof phraseObj.action_speechfallback !== "undefined"){
                    var langExceptions = this.jConfig.services.genaiFallbackToIntents;
                    for (var i = 0; i < langExceptions.length; i++) {
                        if (langExceptions[i] === lang) {
                            doFallback = true;
                            break;
                        }
                    }
                } 

                if (doFallback) {
                    window.oPrsonas.sendKey(phraseObj.action_speechfallback.id);
                    phraseObj._sendKeyDone = true;
                } else {
                    if (typeof phraseObj._raw.properties.answer.qnaId !== "undefined") {
                        intent = this.formatQnaIntent(phraseObj._raw.properties.answer.qnaId);
                    } else {
                        intent = this.formatIntentName(phraseObj._raw.value);
                    }
                    
                    if (phraseObj.answer !== "") {
                        window.oPrsonas.sendKey(intent, phraseObj.answer);
                        phraseObj._sendKeyDone = true;
                    }
                }

            }


            // UX handling
            if (typeof phraseObj.action_ux !== "undefined" && phraseObj.action_ux.context !== ""){
               
                switch(phraseObj.action_ux.context){

                    case "closemodal":
                        window.oModal.close();
                        this.triggerActionEvent(phraseObj);
                        break;                        

                    case "modal":
                        window.oModal.load({
                            id: "mActionUx",
                            content : window.oProjTemplates["m" + phraseObj.action_ux.id](),
                            modalSource: "primary"
                        });
                        window.oLang.applyCurrentLanguage();
                        this.triggerActionEvent(phraseObj);
                        break;

                    case "qrcode":
                        var qrCodeBase64 = window.nc_gfx.getQrCodeBase64(phraseObj.action_ux.id, 4, "L");
                        qrCodeBase64 = 'data:image/png;base64, ' + qrCodeBase64

                        window.oModal.load({
                            id: "mQrCode",
                            content : window.oDxpTemplates.mQrCode({
                                base64: qrCodeBase64,
                                url: phraseObj.action_ux.id
                            }),
                            isBackground: false,
                            modalSource: "primary"
                        });

                        window.oLang.applyCurrentLanguage();
                        this.triggerActionEvent(phraseObj);
                        break;


                    case "intent":
                        if (typeof actionsConfig["intent"] !== "undefined"){
                            window.tEvent.fire(actionsConfig["intent"][phraseObj.action_ux.id]);
                        } 
                        break;

                    case "comment":
                        window.oPrsonas.sendKey(phraseObj.action_ux.id);
                        this.triggerActionEvent(phraseObj);
                        break;

                    case "navigation":
                            switch(phraseObj.action_ux.id){
                                case "back":
                                    if ($(this.ui.backBtn).length !== 0){
                                        window.tEvent.fire("EVENT_PAGE_BACK_BTN");
                                    } else if ($(this.ui.homeBtn).length !== 0) {
                                        window.tEvent.fire("EVENT_PAGE_HOME_BTN");
                                    } else {
                                        return;
                                    }
                                    break;

                                case "home":
                                    if ($(this.ui.homeBtn).length !== 0) {
                                        window.tEvent.fire("EVENT_PAGE_HOME_BTN");
                                    } else {
                                        return;
                                    }
                                    break;

                                default:
                                    this.triggerActionEvent(phraseObj);
                            }

                        break;

                    case "location":

                        if (typeof window.oIha !== "undefined" && window.oIha.oMappedIn !== "undefined" ){

                            if ($(".iha-mappedin-container").css("marginTop") === "-5000px"){
                                window.tEvent.fire("EVENT_PAGE_IHA_MAP", phraseObj.action_ux.id);
                            } else {
                                window.oIha.oMappedIn.showDirections(phraseObj.action_ux.id, phraseObj);
                            }
                            
                        }
                        break;

                    default:
                        this.triggerActionEvent(phraseObj);
                 
                }
            }
        },

        // ______________________________________________________________
        //                                            processSpeechPhrase
        /**
            * processSpeechPhrase
            * @description Process the phrase of the speech to text object to process the context and display the speech to the UX
            * @param {Object} phraseObj - Phrase object from speech to text.
            * @fires processSpeechContext()
            * @memberOf module:managers/speech-manager
            * @instance
        */  
        processSpeechPhrase: function(phraseObj) { 
            console.log(" * <speech.processSpeechPhrase>", phraseObj);

            var self = this;
            var lang = $("body").attr("data-lang");
            var animConfig = this.jConfig.speech.spokenPanel.animation;           
            var confidenceConfig = this.jConfig.services.speechToText.confidence;

            
            if (Number(phraseObj.confidence) < Number(confidenceConfig.min)) {
                console.log(" * <speech.processSpeechPhrase> ignored because of low confidence: " + phraseObj.confidence + " < " + confidenceConfig.min);
                window.oPrsonas.sendKey("NoResponse");
                this.toggleSpeech("enable");
                // ignore
                return;
            } else if (Number(phraseObj.confidence) < Number(confidenceConfig.trigger)){
                console.log(" * <speech.processSpeechPhrase> cant help because of low confidence: " + phraseObj.confidence + " < " + confidenceConfig.trigger);
                window.tEvent.fire(window.tEvent.eventStr.EVENT_CANT_HELP); 
            } else {
                this.processSpeechContext(phraseObj);
            }
            
            // populate spoken copy
            var showStr = phraseObj.spokenStr
            $(this.ui.speakCopy).html(showStr);

            // show spoken panel
            TweenLite.killTweensOf($(this.ui.speakPanel));
            $(this.ui.speakPanel).css("opacity", 0);
            $(this.ui.speakPanel).css("display", "table");
            $(this.ui.speakPanel).css("marginLeft", "-50px");

            TweenLite.to($(this.ui.speakPanel), .3, {opacity: 1, marginLeft: "0px"});

            window.setTimeout(function(){
                TweenLite.to($(self.ui.speakPanel), 2, {opacity: 0, display: "none", onComplete:function(){
                    $(self.ui.speakCopy).html("");
                }});
            }, animConfig.fadeOutTimer);
            
        },

        // --------------------------------------------------------------
        // EVENTS
        // --------------------------------------------------------------
        

        // ______________________________________________________________
        //                                                onQaUpdateCheck
        /**
            * onQaUpdateCheck
            * @description Knowedge Base modified check. Refresh browser if true.
            * @memberOf module:managers/speech-manager
            * @instance
        */
        onQaUpdateCheck: function() {    
            if (this.qaUpdateDate === -1){
                return;
            }

            var qaDate = window.oPrsonas.avatar.getQAProjectInfo().data.lastDeployedDateTime;

            var date1 = moment(qaDate);
            var date2 = moment(this.qaUpdateDate);

            var isAfter = date1.isAfter(this.qaUpdateDate);

            if (isAfter){
                window.location.reload(true);
            }
        },
        

        // ______________________________________________________________
        //                                                   onModelReady
        /**
            * onModelReady
            * @description Initializes class
            * @memberOf module:managers/speech-manager
            * @instance
        */
        onModelReady: function() {    
            // set defines
            this.def  = _.merge(this.def, window.oModel.def);            

            // set jConfig data
            this.jConfig = window.oModel.jConfig;

            this.initSpeech();
        },


        // ______________________________________________________________
        //                                                    onToggleMic
        /**
            * onToggleMic
            * @description Toggle the mic state
            * @param {Boolean} forceState (Optional) forces mic state
            * @fire toggleSpeech()
            * @memberOf module:managers/speech-manager
            * @instance
        */
        onToggleMic: function(forceState) {

            var $micToggle = $(this.ui.micToggle);
            var toggleSpeech = "";
            var micState;

            if (typeof forceState === "undefined"){                
                if ($micToggle.hasClass("is-enabled")){
                    micState = false;                
                } else {
                    micState = true;                
                }           
            } else {
                micState = forceState;     
            }           

            if (micState){
                $micToggle.removeClass("is-disabled");
                $micToggle.addClass("is-enabled");
                this.micEnabled = true;
                toggleSpeech = "enable";
            } else {
                $micToggle.removeClass("is-enabled");
                $micToggle.addClass("is-disabled");
                this.micEnabled = false;
                toggleSpeech = "disable";
            }

            this.toggleSpeech(toggleSpeech);
        },


        // ______________________________________________________________
        //                                         onProcessStatusMessage
        /**
            * onProcessStatusMessage
            * @description Receive and process Prsonas event status message
            * @param {Object} data - Message object
            * @fire toggleSpeech()
            * @memberOf module:managers/speech-manager
            * @instance
        */
        onProcessStatusMessage: function(data) {      

            switch(data.type){
                case "status":
                    if (data.property === "ischaracterspeaking"){
                        data.value = data.value.trim();
                        
                        console.log(" * <speech.onProcessStatusMessage> speech: ", data.value);
                        $("body").attr("data-speaking", data.value);

                        if (data.value === "true"){
                            this.toggleSpeech("disable");  
                        } else {
                            this.toggleSpeech("enable");
                        }
                    }
                    break;
            }

        },

        // ______________________________________________________________
        //                                                      onCcEvent
        /**
            * onCcEvent
            * @description Receives closed caption event
            * @param {Object} data - Message object
            * @memberOf module:managers/speech-manager
            * @fires showCC()
            * @instance
        */
        onCcEvent: function(data){
            console.log(" * <speech.onCcEvent>", data);             

            //display cc copy                    
            this.showCC(data);   
        },

        // ______________________________________________________________
        //                                                   on`Event
        /**
            * onEndCcEvent
            * @description Receives end closed caption event
            * @param {Object} data - Message object
            * @memberOf module:managers/speech-manager
            * @instance
        */
        onEndCcEvent: function(data){

            let doCleanup = false;

            if (typeof data.properties.segment === "undefined") {
                doCleanup = true;
            } else {
                const currentSeg = window.Number(data.properties.segment.split("/")[0]);
                const totalSegs = window.Number(data.properties.segment.split("/")[1]);

                if (currentSeg === totalSegs) { 
                    doCleanup = true; 
                }                
            }

            if (doCleanup) {      

                console.log(" * <speech.onEndCcEvent>", data);

                if (data.value.toLowerCase().indexOf("walkaway") !== -1) {
                    this.doWalkAway();
                } else {

                    var isFound = false;
                    for (let i = 0; i < this.jConfig.intentsToIgnoreMicActivation.length; i++) {
                        if (data.value.toLowerCase() === this.jConfig.intentsToIgnoreMicActivation[i]){
                            isFound = true;
                            break;
                        }
                    }
                    
                    if (!isFound) {
                        this.resetSpeech(true);
                        this.toggleSpeech("enable");                     
                    }
     
                }

                // update speech
                this.onIndicatorUpdate({
                    id: "speech"
                });      

            }


        },



        // ______________________________________________________________
        //                                              onIndicatorUpdate
        /**
            * onIndicatorUpdate
            * @description Receive and process UX updates on status indicator states
            * @param {Object} data - Message object
            * @memberOf module:managers/speech-manager
            * @instance
        */
        onIndicatorUpdate: function(data) {

            switch(data.id){

                case "microphone":
                    window.oConnection.showMicrophoneAccessWarning(data.status);
                    break;

                case "speech":
                    window.oConnection.showSpeechAccessWarning();
                    break;
            }
        },


        // ______________________________________________________________
        //                                       onSpeechPhraseRecognized
        /**
            * onSpeechPhraseRecognized
            * @description Receive and parse phrase from speech to text. 
            * @param {Object} data - Speech data
            * @fires processSpeechPhrase()
            * @memberOf module:managers/speech-manager
            * @instance
        */
        onSpeechPhraseRecognized: function(data) {  

            if (!this.jConfig.speech.isSpeachEnabled){
                return;
            }  

            this.toggleSpeech("disable");

            console.log(" * <speech.onSpeechPhraseRecognized> ", data);

            var responseObj = {
                spokenStr  : data.properties.raw_phrase,
                answer     : data.properties.answer.text,
                confidence : data.properties.answer.metadata.confidence,
                _raw       : data             
            };

            // fix for not being able to submit no answer in the knowledge base
            if (responseObj.answer.trim() === ".") {
                responseObj.answer = "";
            }

            var id = "";

            // action_ux
            if (typeof data.properties.answer.metadata.action_ux !== "undefined"){
                var action = data.properties.answer.metadata.action_ux.split("_");

                responseObj["action_ux"] = {
                    context : action[0].toLowerCase().trim(),
                    id      : action[1].toLowerCase().trim()
                }                  
            }

            // action_speech
            if (typeof data.properties.answer.metadata.action_speech !== "undefined"){
                var action = data.properties.answer.metadata.action_speech.split("_");

                responseObj["action_speech"] = {
                    context : action[0].toLowerCase().trim(),
                    id      : action[1].toLowerCase().trim()
                }                  
            }

            // action_speechfallback
            if (typeof data.properties.answer.metadata.action_speechfallback !== "undefined"){
                var intent = data.properties.answer.metadata.action_speechfallback;

                responseObj["action_speechfallback"] = {
                    context : "",
                    id      : intent
                }                  
            }

            this.processSpeechPhrase(responseObj);
        },


        // ______________________________________________________________
        //                                                     onPageLoad
        /**
            * onPageLoad
            * @description Called when a page is loaded 
            * @param {String} eventId - Event ID
            * @param {Object} data - Event data
            * @memberOf module:managers/speech-manager
            * @instance
        */
        onPageLoad: function(eventId, data) {  

            var self = this;
            var speechState;


            if (this.firstLoad){
                this.firstLoad = false;

                // set initial speech state.        
                this.toggleSpeech("disable");    
            }

            if (!window.oPrsonas.avatar.isSpeaking()) {
                this.resetSpeech();
            }


            // check if we are at the starting page, if do enable forced mic but disable speech
            if (this.jConfig.prsonas.timers.expiredEvent === eventId || this.jConfig.document.startEvent === eventId) {
                this.onToggleMic(true);
                this.toggleSpeech("disable");
            } else {
                for (let key in this.jConfig.proxySendkeyEvents) {
                    if ( (key === this.jConfig.prsonas.timers.expiredEvent || key === this.jConfig.document.startEvent) && this.jConfig.proxySendkeyEvents[key] === eventId){
                        this.onToggleMic(true);
                        this.toggleSpeech("disable");
                    }
                }
            }



             
        }
    };

    /** 
    * @global 
    */
    window.oSpeech = new Speech();
        
    return (window.oSpeech);        
   
});