<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Dmitry</title>
    <description>The latest articles on Forem by Dmitry (@flatorez).</description>
    <link>https://forem.com/flatorez</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F832605%2F1cafe2d0-b978-41b0-9a6a-20fcbe9e7a81.jpeg</url>
      <title>Forem: Dmitry</title>
      <link>https://forem.com/flatorez</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/flatorez"/>
    <language>en</language>
    <item>
      <title>Voice assistant that can be taught how to swear (Part 2)</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Fri, 08 Apr 2022 09:06:03 +0000</pubDate>
      <link>https://forem.com/flatorez/voice-assistant-that-can-be-taught-how-to-swear-part-2-21h5</link>
      <guid>https://forem.com/flatorez/voice-assistant-that-can-be-taught-how-to-swear-part-2-21h5</guid>
      <description>&lt;p&gt;This is the second part of the article about the voice assistant. You can find the first part here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database
&lt;/h2&gt;

&lt;p&gt;Now let’s talk about saving questions and answers. The &lt;a href="https://leetcode.com/problems/implement-trie-prefix-tree/"&gt;Trie&lt;/a&gt; data structure is a great fit for quickly identifying if a question exists in the database and then finding its answer. To store tree nodes and links between them, I used the graph database &lt;a href="https://dgraph.io/"&gt;Dgraph&lt;/a&gt;. For this project, I created a free cloud repository on dgraph.io. A TrieNode looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type TrieNode  {
    id: ID! 
    text: String! 
    isEnd: Boolean!
    isAnswer: Boolean!
    isRoot: Boolean! @search 
    nodes: [TrieNode] 
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The search parameter is needed for the field to be indexed, which  enables us to quickly find the root of the tree by running a query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const query = `
   query {
     roots(func: eq(TrieNode.isRoot, true))
     {
       uid
     }
   }
`;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used &lt;a href="https://github.com/dgraph-io/dgraph-js-http"&gt;dgraph-io/dgraph-js-http&lt;/a&gt; library for sending the requests. To get all child elements for a node, I used the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const query = `
   query all($a: string) {
     words(func: uid($a))
     {
       uid
       TrieNode.nodes {
         uid
         TrieNode.text
         TrieNode.isAnswer
         TrieNode.isEnd
         TrieNode.isRoot
       }
     }
   }
`;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is all it took to traverse the tree depth-first. If the question ends with a word for which there is a node with the isEnd characteristic equal to true, then the answer will be its child element with the value true for the isAnswer field. In addition to query results, dgraph-js-http returns additional information in the extensions field, for instance server_latency, which can be monitored while filling the database with a large number of nodes.&lt;/p&gt;

&lt;p&gt;To configure service access to the database, we need a URL, which can be found at the top of the &lt;a href="https://cloud.dgraph.io/_/dashboard"&gt;main repository page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NFceJiku--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h5888pv9dwv8oxa8el57.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NFceJiku--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h5888pv9dwv8oxa8el57.png" alt="DGraph main repository page" width="880" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second required parameter is an API key. It has to be created in the Settings section, in the API Keys tab: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6TV_rhfc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qydqmoi53m3yavf06a6i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6TV_rhfc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qydqmoi53m3yavf06a6i.png" alt="DGraph API Keys tab" width="880" height="772"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker и Nginx
&lt;/h2&gt;

&lt;p&gt;For ease of development, I added docker and nginx. The corresponding configuration files can be found on github in the &lt;a href="https://github.com/gurfinkel/qsAndAs"&gt;qsAndAs&lt;/a&gt; repository. The three values in the environment section for the service that need to be filled in for everything to work are:&lt;/p&gt;

&lt;p&gt;DGRAPH_HOST - URL for cloud.dgraph.io repository with the question and answer tree without /graphql at the end, should look something like this: &lt;a href="https://somthing.something.eu-central-1.aws.cloud.dgraph.io"&gt;https://somthing.something.eu-central-1.aws.cloud.dgraph.io&lt;/a&gt;;&lt;br&gt;
DGRAPH_KEY - API key from cloud.dgraph.io repository;&lt;br&gt;
GOOGLE_APPLICATION_CREDENTIALS - The path to json-file with the key from the Google Cloud project;&lt;/p&gt;

&lt;h2&gt;
  
  
  Profanity
&lt;/h2&gt;

&lt;p&gt;I decided to use english for the obscenities/profanities.&lt;/p&gt;

&lt;p&gt;First, I checked how Text-to-Speech is protected from the use of English profanity. I changed the phrase "I don't have an answer for you!" to "F$$k off! I don't have an answer for you!" and got the correct audio file without any censorship. Then I asked "Why did that son of a b$tch insult my family?" and got the full transcript again. After that I tried a few phrases such as  "Tony, you motherf$$kers!" from the famous TV series The Sopranos and again everything worked out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The non-conclusion conclusion
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The entire process of creating and testing my project, did not cost me a single penny; &lt;/li&gt;
&lt;li&gt;Speech-to-Text worked perfectly, except for situations where the audio was so poorly legible I had trouble understanding it myself;&lt;/li&gt;
&lt;li&gt;I tried to decipher the an hour-long dialogue between developers by uploading it to Google Cloud Storage. The result was not flawless, but the ability to add adaptive models to the decryption should improve the result;&lt;/li&gt;
&lt;li&gt;Google Cloud was highly convenient to work with, both through the web interface and through &lt;a href="https://cloud.google.com/sdk/docs/install"&gt;gcloud CLI&lt;/a&gt;, I do prefer the interface though;&lt;/li&gt;
&lt;li&gt;I was pleasantly surprised by the availability of a free cloud account for &lt;a href="https://dgraph.io/pricing/"&gt;Dgraph&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;The Dgraph web interface turned out to be very convenient too, and the fact that I could play around with queries and mutations via &lt;a href="https://play.dgraph.io/"&gt;Ratel&lt;/a&gt; greatly accelerated my learning. I must say that before this, I had no opportunity to try working with graph databases;&lt;/li&gt;
&lt;li&gt;In terms of labour intensity, it turned out that a working prototype could easily be made in just one weekend. And taking into account the presence of working examples for accessing Google Cloud for Go, Java, Python, and Node.js, the technologies for the prototype can be chosen from a very wide list; &lt;/li&gt;
&lt;li&gt;In the future, you can replace Trie with a text classifier in &lt;a href="https://cloud.google.com/vertex-ai"&gt;Vertex AI&lt;/a&gt;;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>googlecloud</category>
      <category>dgraph</category>
    </item>
    <item>
      <title>Voice assistant that can be taught how to swear (Part 1)</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Fri, 08 Apr 2022 08:51:44 +0000</pubDate>
      <link>https://forem.com/flatorez/voice-assistant-that-can-be-taught-how-to-swear-part-1-kjj</link>
      <guid>https://forem.com/flatorez/voice-assistant-that-can-be-taught-how-to-swear-part-1-kjj</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I once thought about how hard and costly would it be to create a functional voice assistant that could accurately answer most questions. &lt;/p&gt;

&lt;p&gt;To elaborate, I wanted to create a web-application that records an audio of a question, converts audio to text, finds an answer, and gives it in the audio version. These were the functional requirements which I stated for the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client side
&lt;/h2&gt;

&lt;p&gt;I created a simple &lt;a href="https://reactjs.org"&gt;React&lt;/a&gt; project with &lt;a href="https://github.com/facebook/create-react-app"&gt;create-react-app&lt;/a&gt; and added a component “RecorderAndTranscriber” that contains all the client side functionality. It is worth noting that I used the getUserMedia method from MediaDevices API to get access to the microphone. This access is used by MediaRecorder, which is what we use to record the actual audio. I use setInterval for the timer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--l0TuTKSE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x9zaz3cz20fnwesr5p2d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l0TuTKSE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x9zaz3cz20fnwesr5p2d.png" alt="Main view" width="880" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then we create an empty array as an optional parameter in React hook - useEffect. This array is called only once, when the component is created.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;useEffect(() =&amp;gt; {
    const fetchStream = async function() {
        const stream = await navigator
            .mediaDevices
            .getUserMedia({ audio: true });

        setRecorderState((prevState) =&amp;gt; {
            return {
                ...prevState,
                stream,
            };
        });
    }

    fetchStream();
}, []);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then use the saved stream to create a MediaRecorder instance, which I also save.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;useEffect(() =&amp;gt; {
   if (recorderState.stream) {
       setRecorderState((prevState) =&amp;gt; {
           return {
               ...prevState,
               recorder: new MediaRecorder(recorderState.stream),
           };
       });
   }
}, [recorderState.stream]);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, I added a block to start the counter for seconds elapsed since the start of the recording.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;useEffect(() =&amp;gt; {
    const tick = function() {
        setRecorderState((prevState) =&amp;gt; {
            if (0 &amp;lt;= prevState.seconds 
                &amp;amp;&amp;amp; 59 &amp;gt; prevState.seconds) {
                return {
                    ...prevState,
                    seconds: 1 + prevState.seconds,
                };
            } else {
                handleStop();

                return prevState;
            }
        });
    }

    if (recorderState.initTimer) {
        let intervalId = 
            setInterval(tick, 1000);
        return () =&amp;gt; clearInterval(intervalId);
    }
}, [recorderState.initTimer]);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hook only fires when the initTimer value changes, and callback for setInterval updates the counter value and stops the recording if it lasts more than 60 seconds. This is done since 60 seconds and/or 10Mb are the Speech-to-Text API limits for audio files which can be decrypted when sent directly. Larger files need to be first uploaded into the Google Cloud Storage and processed from there. You can read more about this restriction &lt;a href="https://cloud.google.com/speech-to-text/quotas"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One more thing that has to be mentioned is how the recording is being done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const handleStart = function() {
    if (recorderState.recorder 
        &amp;amp;&amp;amp; 'inactive' === recorderState.recorder.state) {
        const chunks = [];

        setRecorderState((prevState) =&amp;gt; {
            return {
                ...prevState,
                initTimer: true,
            };
        });

        recorderState.recorder.ondataavailable = (e) =&amp;gt; {
            chunks.push(e.data);
        };

        recorderState.recorder.onstop = () =&amp;gt; {
            const blob = new Blob(chunks, 
                { type: audioType });

            setRecords((prevState) =&amp;gt; {
                return [...prevState, 
                    {
                        key: uuid(), 
                        audio: window
                                .URL
                                .createObjectURL(blob), 
                        blob: blob
                    }];
            });
            setRecorderState((prevState) =&amp;gt; {
                return {
                    ...prevState,
                    initTimer: false,
                    seconds: 0,
                };
            });
        };

        recorderState.recorder.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To begin with, I check that an instance of the MediaRecorder class exists and its status is inactive, one of the &lt;a href="https://www.w3.org/TR/mediastream-recording/#recordingstate"&gt;three possible&lt;/a&gt; statuses. Next, the initTimer variable is updated to create and run interval. To control the recording I subscribed to process two events: ondataavailable and onstop. The handler for ondataavailable saves a new piece of audio into a pre-created array. And when onstop fires, a blod file is created from these pieces and is added to the list of ready-to-process recordings. In the recording object, I save the url to the audio file to use audio in the DOM element as a value for src. Blob is used to send the file to the server part of the app. Speaking of which… &lt;/p&gt;

&lt;h2&gt;
  
  
  Server part
&lt;/h2&gt;

&lt;p&gt;To support the client side, I chose to use &lt;a href="https://nodejs.org"&gt;Node.js&lt;/a&gt; and &lt;a href="https://expressjs.com"&gt;​​Express&lt;/a&gt;. I made an index.js file, in which collected th the needed APIs and methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;getTranscription(audio_blob_file)&lt;/li&gt;
&lt;li&gt;getWordErrorRate(text_from_google, text_from_human)&lt;/li&gt;
&lt;li&gt;getAnswer(text_from_google)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To calculate the Word Error Rate I took a python script from the &lt;a href="https://github.com/tensorflow/lingvo/blob/master/lingvo/tasks/asr/tools/simple_wer_v2.py"&gt;tensorflow/lingvo&lt;/a&gt; project and rewrote it in js. In essence, it is just a simple solution of the &lt;a href="https://leetcode.com/problems/edit-distance/"&gt;Edit Distance&lt;/a&gt; task, in addition to error calculation for each of the three types: deletion, insertion, and replacement. In the end, I did not the most intelligent method of comparing texts, and yet it was sufficient enough to later on add parameters to queries to &lt;a href="https://cloud.google.com/speech-to-text/docs/adaptation"&gt;Speech-to-Tex&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For getTranscription I used ready-made code from the documentation for Speech-to-Text, and for the translation of the text answer into the audio file I similarly used code from documentation for &lt;a href="https://cloud.google.com/text-to-speech/docs/libraries"&gt;Text-to-Speech&lt;/a&gt;. The tricky part was to create an access key for Google Cloud from the server part. To start, I had to create a project, then turn on Speech-to-Text API and Text-to-Speech API, create an access key, and finally write the path to the key into the GOOGLE_APPLICATION_CREDENTIALS variable. &lt;/p&gt;

&lt;p&gt;To get a json file with a key, we need to create a Service account for a project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WnvySyO6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z70176ha0bgkb3edb6c4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WnvySyO6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z70176ha0bgkb3edb6c4.png" alt="Google cloud credentials" width="880" height="686"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MgsC5bKE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i45ah0da5uy94fb8yghf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MgsC5bKE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i45ah0da5uy94fb8yghf.png" alt="Google cloud service account" width="880" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After clicking the Create and Continue and Done buttons in the Credentials tab, a new account appears in the  Service Accounts table. If we go to this account, we can click on the Add Key button in the Keys tab and get the json-file with a key. This key is needed to grant the server part of the app access to the Google Cloud services activated in the project.&lt;/p&gt;

&lt;p&gt;I think I will cut the first part of the article here. The next part revolves around the database and experiments with profanity.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>node</category>
      <category>graphql</category>
    </item>
  </channel>
</rss>
