Creando un traductor de un idioma (Español) a otro idioma (Ingles).

Traductor tiempo real usando VUE

En esta entrada he creado un pequeño traductor en tiempo real usando node.js para el servidor, vue.js en la parte front y algo de Javascript para convertir la voz a texto (speech to text) y el texto a voz (text to speech). El código es simple pero los resultados son bastante curiosos.

index.html

<!DOCTYPE html>
<html lang="es">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        [v-cloak] {
            display: none;
        }
        
        textarea {
            resize: none;
        }
    </style>
</head>

<body>
    <main v-cloak>
        <select id="voiceSelect"></select><br>

        <select id="lO">
            <option v-for="(i, t, v) in langOrigen" v-bind="t">{{t}}</option>
        </select>
        <button v-on:click="listenOrigen()">Listen</button><br>
        <textarea cols="30" rows="10" v-model="idiomaOrigen" v-on:keyup="translate()" resize="none"></textarea><br>

        <select id="lD">
            <option v-for="(i, t, v) in langDestino" v-bind="t">{{t}}</option>
        </select>
        <button v-on:click="listenDestino()">Listen</button><br>
        <textarea cols="30" rows="10" v-text="idiomaDestino" resize="none" disabled></textarea><br>

        <span v-text="info"></span><br>
        <button id="start-record-btn" title="Start Recording" v-show="mostrar" v-on:click="startSpeech()">Iniciar reconocimiento voz a texto</button>
        <button id="pause-record-btn" title="Pause Recording" v-show="!mostrar" v-on:click="stopSpeech()">Parar Iniciar reconocimiento voz a texto</button>
    </main>
    <script src="./vue.js"></script>
    <script src="./httpVueLoader.js"></script>
    <script src="./axios.min.js"></script>
    <script src="./languages.js"></script>
    <script src="./main.js"></script>
</body>

</html>

main.js

new Vue({
    el: "main",
    data: {
        idiomaOrigen: null,
        idiomaDestino: null,
        info: "Reconocimiento de voz parada",
        mostrar: true,
        noteContent: "",
        recognition: null,
        lang: null,
        langOrigen: null,
        langDestino: null
    },
    created() {},
    mounted() {
        this.rellenaLang();
        this.speechToText();
    },
    methods: {
        rellenaLang: function() {
            debugger;
            this.langOrigen = langs;
            this.langDestino = langs;
        },
        populateVoiceList: function() {
            if (typeof speechSynthesis === 'undefined') {
                return;
            }
            voices = speechSynthesis.getVoices();
            for (i = 0; i < voices.length; i++) {
                var option = document.createElement('option');
                option.textContent = voices[i].name + ' (' + voices[i].lang + ')';

                if (voices[i].default) {
                    option.textContent += ' -- DEFAULT';
                }

                option.setAttribute('data-lang', voices[i].lang);
                option.setAttribute('data-name', voices[i].name);
                document.getElementById("voiceSelect").appendChild(option);
            }
        },

        listenDestino: function() {
            this.readOutLoud(this.idiomaDestino);
        },
        listenOrigen: function() {
            this.readOutLoud(this.idiomaOrigen);
        },
        translate: function() {
            debugger;
            var _this = this;
            if (_this.idiomaOrigen.length > 0) {
                var lO = document.getElementById("lO")[document.getElementById("voiceSelect").selectedIndex].getAttribute("data-lang");
                var lD = document.getElementById("lD")[document.getElementById("voiceSelect").selectedIndex].getAttribute("data-lang");
                axios.get("http://localhost:3002/translate/" + _this.idiomaOrigen)
                    .then(function(respuesta) {
                        _this.idiomaDestino = respuesta.data;
                    })
                    .catch(function(error) {
                        console.log(error);
                    });
            } else {
                _this.idiomaDestino = null;
            }
        },
        readOutLoud: function(message) {
            this.lang = document.getElementById("voiceSelect")[document.getElementById("voiceSelect").selectedIndex].getAttribute("data-lang");
            var speech = new SpeechSynthesisUtterance();
            speech.text = message;
            speech.volume = 1;
            speech.rate = 1;
            speech.pitch = 1;
            speech.lang = this.lang;
            window.speechSynthesis.speak(speech);
        },
        startSpeech: function() {
            if (this.noteContent.length) {
                this.noteContent += ' ';
            }
            this.recognition.start();
            this.info = 'Reconocimiento de voz inciada';
            this.mostrar = false;
        },
        stopSpeech: function() {
            this.recognition.stop();
            this.info = 'Reconocimiento de voz parada';
            this.mostrar = true;
        },
        speechToText: function() {
            var _this = this;
            try {
                var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
                _this.recognition = new SpeechRecognition();
                _this.populateVoiceList();
                if (typeof speechSynthesis !== 'undefined' && speechSynthesis.onvoiceschanged !== undefined) {
                    speechSynthesis.onvoiceschanged = _this.populateVoiceList;
                }
            } catch (e) {
                _this.info = "Error: " + e;
            }
            _this.recognition.continuous = true;
            _this.recognition.onresult = function(event) {
                var current = event.resultIndex;
                var transcript = event.results[current][0].transcript;
                var mobileRepeatBug = (current == 1 && transcript == event.results[0][0].transcript);
                if (!mobileRepeatBug) {
                    _this.noteContent += transcript;
                    _this.idiomaOrigen = _this.noteContent;
                    _this.translate();
                    _this.readOutLoud(_this.noteContent);
                }
            };
            _this.recognition.onstart = function() {
                _this.info = 'Reconocimiento de voz activado. Prueba hablar al microfono.';
            }
            _this.recognition.onspeechend = function() {
                _this.info = 'Debes estar en silencio para que el reconocimiento de voz se pare.';
            }
            _this.recognition.onerror = function(event) {
                if (event.error == 'no-speech') {
                    _this.info = 'No se detecto a nadie hablando. Prueba de nuevo.';
                };
            }

        }
    }
});

server.js (nodeJS)

var express = require('express');
var app = express();
var translate = require('google-translate-api');
var port = 3002;

app.use(function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    next();
});

app.get('/translate/:frase?', function(req, res) {
    var _res = res;
    translate(req.params.frase, { to: 'en' }).then(function(res) {
        console.log(res.text);
        _res.send(res.text);
    }).catch(function(err) {
        console.error(err);
    });
});
app.get("/test/", function(req, res) {
    res.send("Test Node.JS");
});
app.listen(port);
console.log('Listening on port ' + port);

Para poder realizar la magia que se crea en nodeJS he necesitado las siguientes dependencias:

  • express: npm i express –save
  • google-translate-api: npm i google-translate-api –save