Magento 2 – Voice Search

Javascript Web Speech API has been an essential part of the browser as it makes it easy to add speech recognition to web pages. This API has speech recognition capabilities in browsers to recognize spoken text immediately. Most of the browsers are compatible with support for Web Speech API.

Advantages of adding Voice Search in Magento Store:

It attracts the customer to make searching based on speech.

Customers who have certain disabilities and can’t use a keyboard or mouse can easily speak and access the store.

Customers with temporary physical limitations such as a broken arm or hands can access the store.

It increases the chances of making more sales.

Adding Voice Search into Magento 2:

Generally, Web Speech API has 2 main components:

  1. Voice Synthesis
  2. Speech Recognition

Important Note: If you do not want to develop this extension yourself, you can use a readymade extension made by BIZSPICE. It’s distributed absolutely FREE.

Extension Download Linkhttps://www.dropbox.com/sh/z9cf6qx5o961taa/AABzCRJOTrgx3Q-fNRSG1Q3Aa?dl=0

Last Updated: 07/12/2018

Version Supported: 2.2.5 and higher

Here, we will work on Speech Recognition. Following are the steps:

  1. Creating and registering a new Custom Voice Search Module
  2. Defining Custom Voice Search Details
  3. Defining Custom Settings
  4. Creating a Class to Save Image
  5. Creating Custom Mini Search Form
  6. Creating Javascript File to make Voice Search Enabled
  7. Creating CSS File to add custom CSS to manage Mic images
  8. Running Magento Commands

Create And Register A New Custom Voice Search Module:

To create a custom module, the very first step is to create a file, module.xml in the folder, <Magento Root Folder>/app/code /<Vendor Name>/<Module Name>/etc/. Let’s take the path –

/app/code/Bizspice/VoiceSearch/etc/module.xml

and paste the following code into this file:

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
              <module name="Bizspice_VoiceSearch" setup_version="1.0">
               <sequence>
            <module name="Magento_CatalogSearch"/>
      </sequence>
  </module>
</config>

Now, create a file registration.php in /app/code/Bizspice/VoiceSearch/ folder with the following code:

\Magento\Framework\Component\ComponentRegistrar::register( 
 \Magento\Framework\Component\ComponentRegistrar::MODULE,
     'Bizspice_VoiceSearch',
     __DIR__
 ); 

Define Custom Voice Search Details:

Create a file config.xml in /app/code/Bizspice/Shippingmodule/etc/ folder with the following code:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <bizspicevoicesearch>
            <general>
                <enable>1</enable>
            </general>
        </bizspicevoicesearch>
    </default>
</config>

Here, in config.xml file, parent tag is default and child tag is bizspicevoicesearch which is having enable field set to 1 so that its value in Admin panel would be Yes by default.

Define Custom Settings In Admin Panel

There will be some configuration options for our custom VoiceSearch module in admin panel. To manage these options, create a file system.xml in /app/code/Bizspice/VoiceSearch/etc/adminhtml/ folder with the following code:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../Config/etc/system_file.xsd">
   <system>
       <tab id="bizspice" translate="label" sortOrder="100">
           <label>BIZSPICE</label>
        </tab>
        <section id="bizspicevoicesearch" translate="label" type="text" sortOrder="140" showInDefault="1" showInWebsite="1" showInStore="0">
            <label>Voice Search</label>
            <tab>bizspice</tab>
            <resource>Bizspice_VoiceSearch::configuration</resource> 
            <group id="general" translate="label" type="text"  sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Voice Search Settings</label>
                <field id="enable" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="mic_off" translate="label" type="image" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Off Mic Icon</label>
                    <backend_model>Bizspice\VoiceSearch\Model\Saveimage</backend_model>
                    <base_url type="media" scope_info="1">bizspice/voicesearch</base_url>
                    <comment><![CDATA[Allowed file types: jpg, jpeg, gif, png, svg]]></comment>
                </field>
                <field id="mic_on" translate="label" type="image" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>On Mic Icon</label>
                   <backend_model>Bizspice\VoiceSearch\Model\Saveimage</backend_model>
                   <base_url type="media" scope_info="1">bizspice/voicesearch</base_url>
                    <comment>
                        <![CDATA[Icon will be displayed while listening user's voice <br>Allowed file types: jpg, jpeg, gif, png, svg]]>
                    </comment>
                </field>
            </group>
        </section>
    </system>
</config>

Create A Class To Save Image Of Mic-On And Mic-Off

As you can see above in the system.xml,  Bizspice\VoiceSearch\Model\Saveimage has been specified as backend_model to save the images of mic-on and mic-off under bizspice/voicesearch folder. So, here is the class for /app/code/Bizspice\VoiceSearch\Model\Saveimage.php:

<?php
namespace Bizspice\VoiceSearch\Model;
class Saveimage extends \Magento\Config\Model\Config\Backend\Image
{
    /**
     * The tail part of directory path for uploading
     */
     const UPLOAD_DIR = 'bizspice/voicesearch';
     /**
     * Upload max file size in kilobytes
     *
     * @var int
     */
     protected $_maxFileSize = 2048;
     /**
     * Return path to directory for upload file
     *
     * @return string
     */
     protected function _getUploadDir(){
        return $this->_mediaDirectory->getAbsolutePath(self::UPLOAD_DIR);
    }
     /**
     * Makes a decision about whether to add info about the scope
     *    
     * @return boolean
     */
     protected function _addWhetherScopeInfo(){
        return false;
     }
     protected function _getAllowedExtensions(){
        return ['jpg', 'jpeg', 'gif', 'png', 'svg'];
     }
     /**   
     * Save uploaded file before saving config value
     *
     * Save changes and delete file if "delete" option passed
     *
     * @return $this
     */
     public function beforeSave(){
         $value = $this->getValue();
         $deleteFlag = is_array($value) && !empty($value['delete']);
         $fileTmpName = $_FILES['groups']['tmp_name'][$this->getGroupId()]['fields'][$this->getField()]['value'];
         if ($this->getOldValue() && ($fileTmpName || $deleteFlag)){
            $this->_mediaDirectory->delete(self::UPLOAD_DIR . '/' . $this->getOldValue());
          }
         return parent::beforeSave();
     }
}

Create Custom Minni Search Form

Now, to add voice searching in the mini search form in the header, some customizations have to be done in this form. Therefore, we have to create a custom form and that form has to be set as template for Mini Search Form in header. So, create the following files:

default.xml file under  /app/code/Bizspice/VoiceSearch/view/frontend/layout/ folder with the following code:

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="top.search">
            <action method="setTemplate" ifconfig="bizspicevoicesearch/general/enable">
                <argument name="template" xsi:type="string">Bizspice_VoiceSearch::form.mini.phtml</argument>
            </action>
        </referenceBlock>
    </body>
</page>

Here, in this file, we have defined the top.search form so that it must fetch the template of the form from-

/app/code/Bizspice/VoiceSearch/view/frontend/templates/form.mini.phtml  

and

block for fetching custom settings of the form has been defined at –

/app/code/Bizspice/VoiceSearch/Block/Form.phpSo, here is the code of /app/code/Bizspice/VoiceSearch/Block/Form.php:

<?php
namespace Bizspice\VoiceSearch\Block;
use Bizspice\VoiceSearch\Model\Saveimage;
use \Magento\Store\Model\StoreManagerInterface;
class Form extends \Magento\Framework\View\Element\Template {
    protected $_registry;
    protected $_mediaURL = "";
    protected $_storeManager;
    public $_resolver, $_micOff, $_micListening;
    public function __construct(
        \Magento\Backend\Block\Template\Context $context,        
        \Magento\Framework\Registry $registry,
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        StoreManagerInterface $storeManager,
        \Magento\Framework\Locale\Resolver $resolver,
       array $data = []
    ) {        
        $this->_registry = $registry;
        parent::__construct($context, $data);	
        $this->_storeManager = $storeManager;
        $this->_resolver = $resolver;
        $this->_mediaURL = $this->_storeManager->getStore()->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA);
        $imageURL = $this->_mediaURL.Saveimage::UPLOAD_DIR."/";
        $this->_micOff = $this->_scopeConfig->getValue('bizspicevoicesearch/general/mic_off', \Magento\Store\Model\ScopeInterface::SCOPE_STORE);
        if(!empty($this->_micOff)) {
            $this->_micOff = $imageURL.$this->_micOff;
        }
        else {
            $this->_micOff = $this->escapeUrl($this->getViewFileUrl('Bizspice_VoiceSearch::images/mike_off.svg'));
        }
        $this->_micListening = $this->_scopeConfig->getValue('bizspicevoicesearch/general/mic_on', \Magento\Store\Model\ScopeInterface::SCOPE_STORE);
        if(!empty($this->_micListening)) {
            $this->_micListening = $imageURL.$this->_micListening;
        }
        else {
            $this->_micListening = $this->escapeUrl($this->getViewFileUrl('Bizspice_VoiceSearch::images/listening.svg'));
        }
    }
    public function _prepareLayout() {
        return parent::_prepareLayout();
    }
}

And here, is the code of /app/code/Bizspice/VoiceSearch/view/frontend/templates/form.mini.phtml:

<?php
$helper = $this->helper(\Magento\Search\Helper\Data::class);
$blockObj= $block->getLayout()->createBlock('Bizspice\VoiceSearch\Block\Form');
$storeLanguage = $blockObj->_resolver->getLocale();
$micOffImage = $blockObj->_micOff;
$onIconImage = $blockObj->_micListening;
?> 
<style type="text/css">
.micOffImage {
    background: url("<?php echo $micOffImage; ?>") no-repeat;   
} 
.onIconImage {    
    background: url("<?php echo $onIconImage;?>") no-repeat;
}
</style>
<script type="text/javascript">
    window.storeLanguage = "<?php echo $storeLanguage; ?>";
</script>
<div class="block block-search">
    <div class="block block-title"><strong><?=  __('Search') ?></strong></div>
    <div class="block block-content">
        <form class="form minisearch" id="search_mini_form" action="<?=  $helper->getResultUrl() ?>" method="get">
            <div class="field search">
                <label class="label" for="search" data-role="minisearch-label">
                    <span><?=  __('Search') ?></span>
                </label>
                <div class="control">
                    <input id="search"
                        data-mage-init='{"quickSearch":{
                        "formSelector":"#search_mini_form",
                        "url":"<?=  $helper->getSuggestUrl()?>",
                        "destinationSelector":"#search_autocomplete"},
                        "voiceSearch":{}
                        }'
                        type="text"
                        name="<?=  $helper->getQueryParamName() ?>"
                        value="<?=  $helper->getEscapedQueryText() ?>"
                        placeholder="<?=  __('Search entire store here...') ?>"
                        class="input-text"
                        maxlength="<?=  $helper->getMaxQueryLength() ?>"
                        role="combobox"
                        aria-haspopup="false"
                        aria-autocomplete="both"
                        autocomplete="off"
                        />
                        <i class="desktopMic micOffImage voice-search"><span></span></i>
                    <div id="search_autocomplete" class="search-autocomplete"></div>
                    <?= $block->getChildHtml() ?>
                     <span class="iconMicOff micOffImage voice-search bs-moble-style"></span>
                    <!-- ******** -->
                </div>
            </div>
            <div class="actions">
                <button type="submit"
                        title="<?= $block->escapeHtml(__('Search')) ?>"
                        class="action search">
                    <span><?=  __('Search') ?></span>
                </button>
            </div>
        </form>
    </div>
</div>

Create A Javascript File To Enable Voice Search

To enable Voice Searching in the front-end, you have to add Web Speech API code. So, create /app/code/Bizspice/VoiceSearch/view/frontend/requirejs-config.js with the following code:

var config = {
    map: {
        '*': {
            voiceSearch: 'Bizspice_VoiceSearch/js/voiceSearch'
        }
    }
};

Here, you can see in this file voiceSearch JS file has been included. Create this file at –/app/code/Bizspice/VoiceSearch/view/frontend/web/js/voiceSearch.js with the following code:

define([
        'jquery',
        'domReady!'
    ], function ($) {
    'use strict';
    var $voiceSearchTrigger = $(".micOffImage");
    var $voiceSearchMicOn = $(".onIconImage");
    var $formMiniSearch = $("#search_mini_form");
    var $searchInput = $("#search");
    window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (window.SpeechRecognition) {
        $(".voice-search").removeClass("icon_mic_show");
    }
    function _parseTranscript(e)
    {
        return Array.from(e.results).map(result => result[0]).map(result => result.transcript).join('');
    }
    function _transcriptHandler(e) {
        $searchInput.val(_parseTranscript(e));
        if (e.results[0].isFinal) {
            $formMiniSearch.submit();
        }
    }
 
    if (window.SpeechRecognition) {
        var recognition = new SpeechRecognition();
        recognition.interimResults = true;
        recognition.addEventListener('result', _transcriptHandler);
        recognition.addEventListener('start', _speechStart);
    } else {
        alert("Speech Recognition is not supported in your browser or it has been disabled.");
    }
    function _speechStart() {
        console.log('BS Speech Started');
        $(".voice-search").removeClass("micOffImage");
        $(".voice-search").addClass("onIconImage");
        $(".voice-search").addClass("listening");
        $(".bs-moble-style").addClass("iconListening");
        $(".bs-moble-style").removeClass("iconMicOff");
    };
    function startListening(e) {
        e.preventDefault();
       e.preventDefault();
        if ($searchInput.attr("placeholder") == "Listening...") {
            recognition.stop();
            $(".voice-search").removeClass("onIconImage");
            $(".voice-search").addClass("micOffImage");
            $(".voice-search").removeClass("listening");
            $(".bs-moble-style").removeClass("iconListening");
            $(".bs-moble-style").addClass("iconMicOff");
            $searchInput.attr("placeholder", "Search entire store here...");
        } else {
            recognition.start();
            $searchInput.attr("placeholder", "Listening...");
        }
    }
    jQuery(".form.minisearch label").on("click", function(){
        jQuery("#search_mini_form input#search").unbind("blur");
    });

    return function() {
        $voiceSearchTrigger.on('click touch', startListening);
     }
});

Create CSS File To Add Custom CSS To Manage Mic Images

To add custom CSS, create /app/code/Bizspice/VoiceSearch/view/frontend/web/css/source/_module.less file with the following code:

.desktopMic {
     display: inline-block;
    -moz-box-sizing: content-box;
    border: 0;
    box-shadow: none;
    line-height: inherit;
    margin: 0;
    padding: 0;
    text-decoration: none;
    text-shadow: none;
    font-weight: 400;
    position: absolute;
    right: 34px;
    top: 6px;
    z-index: 1;
    content: '';
    background-size: 18px 18px;
    background-position: center;
}
.desktopMic::before{
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    font-size: 16px;
    line-height: 32px;
    color: #757575;
    content:  '';
    width: 100%;
    margin: 0;
    vertical-align: top;
    display: inline-block;
    font-weight: normal;
    overflow: hidden;
    speak: none;
    padding: 10px;
    text-align: center;
}
.block-search .label.searchshow+.control input {
    position: static;
}
.listening{
    background-size: 50px 50px !important;
}
.header.content:after {
   display: inline !important;
}
@media(max-width: 767px){
    .desktopMic{display:none;}
}
/******************************/
.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) {
    .block-search .label.active+.control .iconListening::before{
        -webkit-font-smoothing: antialiased;
        content: '';
        margin: 0;
        display: inline-block;
        overflow: hidden;
        speak: none;
        text-align: center;
        top:100px;
        padding: 20px;
    }
    .block-search .label.active+.control .iconMicOff::before{
        -webkit-font-smoothing: antialiased;
        content: '';
        margin: 0;
        display: inline-block;
        overflow: hidden;
        speak: none;
        text-align: center;
        top:100px;
        padding-right: 20px;
        background-position: center;
    }
    .iconMicOff{
        background-size: 18px 18px !important;
        position: relative;
        float: right;
        top: -40px;
    }
    .iconListening{
        position: relative;
        float: right;
        top: -55px;
    }
    .desktopMic{
        display:none !important;
    }
}

There will be 2 default images of Mic-On and Mic-Off and should be placed at –

  • /app/code/Bizspice/VoiceSearch/view/frontend/web/images/mike_off.svg
  • /app/code/Bizspice/VoiceSearch/view/frontend/web/images/listening.svg

Running Magento Commands:

Custom Module has been created and now the last step is to install this module and clear the cache (if enabled in Magento) for using this module. Run the following commands:

rm -rf pub/static/* var/* generated/*
chmod 0777 var/ pub/ generated/ 
chmod 0777 -R var/ generated/ pub/
php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento setup:static-content:deploy -f
php bin/magento indexer:reindex
php bin/magento cache:clean
php bin/magento cache:flush

Now, Custom Voice Search Module is ready to configure and use. To enable and configure it, go to –
Magento 2 Admin panel > STORES > Configuration > BIZSPICE > Voice Search. Here, you will see the following –

Here, admin can enable/disable the module and set the desired Mic Icons.