Commit 35ee9638 authored by Paolo Greppi's avatar Paolo Greppi

add TTS (test-to-speech)

parent 00763028
......@@ -13,7 +13,12 @@
<div class="clearfix">
<h2 class="float-left">
<selection v-bind:to_read="article.to_read" v-bind:id="article.id"></selection>
<span v-if="article.language != 'it' && article.title" v-bind:lang="article.language">[{{ article.title_original }}] </span><span v-bind:lang="article.title ? 'it' : article.language">{{ article.title || article.title_original }}</span>
<template v-if="article.language != 'it' && article.title">
[<span v-bind:lang="article.language" id="title_tts">{{ article.title_original }}</span>] <span lang="it">{{ article.title }}</span>
</template>
<template v-else>
<span v-bind:lang="article.title ? 'it' : article.language" id="title_tts">{{ article.title || article.title_original }}</span>
</template>
</h2>
</div>
<div class="row">
......@@ -50,14 +55,25 @@
</div>
</div>
<download-send v-bind:ids="article.id"></download-send>
<button type="button" id="button-speak" class="btn btn-success btn-sm" title="Leggi ad alta voce" hidden v-on:click="tts_speak();"><i class="fa fa-play" aria-hidden="true"></i></button>
<div style="position: fixed; z-index: 1000; bottom: 1em; left: 1em;" class="btn-group btn-group-sm" id="button-tts" role="group" aria-label="Leggi ad alta voce" hidden >
<button type="button" id="button-restart" class="btn btn-success" title="Dall'inizio" v-on:click="tts_restart();"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
<button type="button" id="button-back" class="btn btn-success" title="Indietro" v-on:click="tts_back();"><i class="fa fa-backward" aria-hidden="true"></i></button>
<button type="button" id="button-stop" class="btn btn-success" title="Stop" v-on:click="tts_stop();"><i class="fa fa-pause" aria-hidden="true"></i> </button>
<button type="button" id="button-continue" class="btn btn-success disabled" title="Continua" v-on:click="tts_continue();"><i class="fa fa-play" aria-hidden="true"></i></button>
<button type="button" id="button-forward" class="btn btn-success" title="Avanti" v-on:click="tts_forward();"><i class="fa fa-forward" aria-hidden="true"></i></button>
<button type="button" id="button-close" class="btn btn-success" title="Chudi" v-on:click="tts_close();"><i class="fa fa-close" aria-hidden="true"></i></button>
</div>
<hr/>
<div class="row">
<template v-if="article.language != 'it' && article.content">
<div class="col-sm-6" v-html="article.content_original" v-bind:lang="article.language"></div>
<div class="col-sm-6" v-html="article.content_original" v-bind:lang="article.language" id="content_tts"></div>
<div class="col-sm-6" v-html="article.content"></div>
</template>
<template v-else>
<div class="col-sm-12" v-html="article.content || article.content_original" v-bind:lang="article.content ? 'it' : article.language"></div>
<div class="col-sm-12" v-html="article.content || article.content_original" v-bind:lang="article.content ? 'it' : article.language" id="content_tts"></div>
</template>
</div>
<hr/>
......@@ -100,7 +116,7 @@
<script type="text/javascript">
// configuration for jshint
/* jshint browser: true, devel: true */
/* global Vue, getRandomInt, icons, feeds, secondsToString, getParameterByName, api, font_size, idbKeyval, $ */
/* global Vue, getRandomInt, icons, feeds, secondsToString, getParameterByName, api, font_size, idbKeyval, $, tts_init, current_paragraph: true, stopped: true, read_paragraph, tts_stop */
"use strict";
......@@ -124,12 +140,24 @@ var Article = {
console.log('Article created');
this.loadData();
},
mounted: function () {
console.log('Article mounted');
},
activated: function() {
console.log('Article activated');
var button_tts = document.getElementById('button-tts');
if (button_tts) button_tts.hidden = true;
var button_speak = document.getElementById('button-speak');
if (button_speak) button_speak.hidden = true;
this.active = true;
},
deactivated: function() {
console.log('Article deactivated');
tts_stop();
var button_tts = document.getElementById('button-tts');
if (button_tts) button_tts.hidden = true;
var button_speak = document.getElementById('button-speak');
if (button_speak) button_speak.hidden = true;
this.active = false;
},
watch: {
......@@ -207,6 +235,9 @@ var Article = {
bus.$emit('read', self.$route.params.id);
var article_cached = self.article.cached;
self.article = JSON.parse(xhr.responseText);
Vue.nextTick(function () {
tts_init();
});
self.loading = false;
self.article.cached = Date.now();
idbKeyval.set('article_' + self.article.id, self.article);
......@@ -239,6 +270,60 @@ var Article = {
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader("accept", "application/json");
xhr.send();
},
tts_speak: function() {
console.log('speak button pressed');
current_paragraph = 0;
stopped = false;
read_paragraph();
document.getElementById('button-tts').hidden = false;
document.getElementById('button-speak').hidden = true;
},
tts_stop: function() {
stopped = true;
window.speechSynthesis.cancel();
document.getElementById('button-stop').classList.add('disabled');
document.getElementById('button-continue').classList.remove('disabled');
console.log('Speaker stopped ' + current_paragraph);
},
tts_continue: function() {
stopped = false;
document.getElementById('button-stop').classList.remove('disabled');
document.getElementById('button-continue').classList.add('disabled');
read_paragraph();
},
tts_restart: function() {
stopped = true;
window.speechSynthesis.cancel();
current_paragraph = 0;
stopped = false;
document.getElementById('button-stop').classList.remove('disabled');
document.getElementById('button-continue').classList.add('disabled');
read_paragraph();
},
tts_back: function() {
stopped = true;
window.speechSynthesis.cancel();
current_paragraph -= 1;
stopped = false;
document.getElementById('button-stop').classList.remove('disabled');
document.getElementById('button-continue').classList.add('disabled');
read_paragraph();
},
tts_forward: function() {
stopped = true;
window.speechSynthesis.cancel();
current_paragraph += 1;
stopped = false;
document.getElementById('button-stop').classList.remove('disabled');
document.getElementById('button-continue').classList.add('disabled');
read_paragraph();
},
tts_close: function() {
stopped = true;
window.speechSynthesis.cancel();
document.getElementById('button-tts').hidden = true;
document.getElementById('button-speak').hidden = false;
}
}
};
......
......@@ -43,4 +43,5 @@
<?php require 'selection-template.html'; ?>
<?php require 'news-list-template.php'; ?>
<script type="text/javascript" defer src="/js/index.js"></script>
<script type="text/javascript" src="/js/tts.js"></script>
<?php require 'footer.php'; ?>
// configuration for jshint
/* jshint browser: true, devel: true */
/* global $ */
"use strict";
// returns the voice
function find_voice(voices, lang) {
console.log('found ' + voices.length + ' voices');
var voices_filtered = voices.filter(function(voice) {
return voice.lang == lang;
});
if (voices_filtered.length == 0) {
// try matching only 2-character ISO code
voices_filtered = voices.filter(function(voice) {
return voice.lang.substr(2) == lang;
});
}
console.log('found ' + voices_filtered.length + ' ' + lang + ' voices');
if (voices_filtered.length == 0) {
return null;
} else if (voices_filtered.length == 1) {
return voices_filtered[0];
} else {
var voices_filtered1 = voices_filtered.filter(function(voice) {
return voice.default;
});
console.log('found ' + voices_filtered1.length + ' ' + lang + ' default voices');
if (voices_filtered1.length > 0) {
return voices_filtered1[0];
} else {
return voices_filtered[0];
}
}
} // find_voice
function getParagraphs(element) {
var ps = [];
element.childNodes.forEach(function(child) {
if (child.nodeName == 'P' || child.nodeName == 'H1' || child.nodeName == 'H2' || child.nodeName == 'H3' || child.nodeName == 'H4' || child.nodeName == 'H5') {
ps = ps.concat([child]);
} else {
var sub = getParagraphs(child);
if (sub.lenght > 0) {
ps = ps.concat(sub);
} else {
ps = ps.concat([child]);
}
}
});
return ps;
} // getParagraphs
var current_paragraph = 0;
var stopped = false;
var paragraphs = [];
var voice = null;
var lang = '';
function read_paragraph() {
console.log('Reading ' + current_paragraph);
if (paragraphs.length <= current_paragraph) {
current_paragraph = paragraphs.length;
document.getElementById('button-forward').classList.add('disabled');
document.getElementById('button-stop').classList.add('disabled');
document.getElementById('button-continue').classList.add('disabled');
return;
}
if (current_paragraph < 0)
current_paragraph = 0;
if (current_paragraph == 0) {
document.getElementById('button-restart').classList.add('disabled');
document.getElementById('button-back').classList.add('disabled');
} else {
document.getElementById('button-restart').classList.remove('disabled');
document.getElementById('button-back').classList.remove('disabled');
}
if (paragraphs.length == current_paragraph + 1) {
document.getElementById('button-forward').classList.add('disabled');
} else {
document.getElementById('button-forward').classList.remove('disabled');
}
var paragraph = paragraphs[current_paragraph];
if ('innerText' in paragraph) {
var utterance = new window.SpeechSynthesisUtterance();
utterance.voice = voice;
utterance.lang = voice.lang;
utterance.text = paragraph.innerText;
utterance.addEventListener('start', function() {
console.log('Speaker started ' + current_paragraph);
document.getElementById('button-continue').classList.add('disabled');
document.getElementById('button-back').classList.remove('disabled');
paragraph.style.backgroundColor = 'yellow';
paragraph.scrollIntoView();
});
utterance.addEventListener('end', function(e) {
console.log('Speaker finished ' + current_paragraph + ' in ' + e.elapsedTime + ' seconds.');
paragraph.style.backgroundColor = 'white';
document.getElementById('button-continue').classList.remove('disabled');
document.getElementById('button-back').classList.add('disabled');
if (!stopped) {
current_paragraph += 1;
read_paragraph(voice, lang);
}
});
window.speechSynthesis.speak(utterance);
} else {
current_paragraph += 1;
read_paragraph(voice, lang);
}
} // read_paragraph
function tts_init() {
if ('speechSynthesis' in window) {
console.log('TTS API available');
var title_tts = document.getElementById('title_tts');
var content_tts = document.getElementById('content_tts');
paragraphs = [title_tts].concat(getParagraphs(content_tts));
lang = content_tts.getAttribute('lang');
console.log('looking for voices with lang = ' + lang);
voice = find_voice(window.speechSynthesis.getVoices(), lang);
console.log('voice = ', voice);
if (voice) {
document.getElementById('button-speak').hidden = false;
console.log('ready');
} else {
console.log('no voice found !');
document.getElementById('button-speak').hidden = true;
} // voice found
} else {
console.log('no TTS API !');
document.getElementById('button-speak').hidden = true;
}
} // tts_init
function tts_stop() {
if ('speechSynthesis' in window) {
stopped = true;
window.speechSynthesis.cancel();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment