Added experimental speech support.

This commit is contained in:
doncr 2021-02-16 13:49:44 +00:00
parent ad88fb3642
commit 8daf3d81da
3 changed files with 198 additions and 9 deletions

View File

@ -303,3 +303,12 @@ button.btn.btn-primary {
.btn-primary.focus, .btn-primary:focus {
box-shadow: 0 0 0 .2rem #0060c080;
}
#voiceSelectError {
color: red;
}
div.halfSpeed {
font-size: 50%;
text-align: center;
}

View File

@ -63,6 +63,12 @@
</div>
</div>
<div id="voiceSelectError" style="display: none">
<div class="row justify-content-center mb-2 ml-2 mr-2">
<div>You must select a voice first for speech synthesis.</div>
</div>
</div>
<div class="container options">
<div class="row">
@ -143,6 +149,16 @@
<div class="text-center mb-4">Question pool size: <span id="questionCount">...</span></div>
<div class="row justify-content-center mt-3 ml-2 mr-2">
<div id="voice_select_options" class="form-check" style="display: none">
<label class="form-check-label" for="voice_select">Select voice (Notice: Online voices may incur data charges!)</label>
<br>
<div>Also: Non-Japanese voices may not work at all.</div>
<br>
<select id="voice_select"><option><i>Select voice...</i></option></select>
</div>
</div>
<div class="row justify-content-center mt-3 ml-2 mr-2">
<div class="form-group">
<div class="form-check"><input class="form-check-input" type="checkbox" id="trick" checked><label
@ -152,6 +168,8 @@
class="form-check-label" for="kana">Use hiragana throughout the test (no kanji)</label></div>
<div class="form-check"><input class="form-check-input" type="checkbox" id="furigana_always" checked><label
class="form-check-label" for="furigana_always">Show furigana on questions</label></div>
<div class="form-check" id="useVoiceSection" style="display: none"><input class="form-check-input" type="checkbox" id="use_voice"><label
class="form-check-label" for="use_voice">Use speech synthesis (experimental)</label></div>
</div>
</div>
@ -218,7 +236,7 @@
<div class="col-12 text-center" id="correction"></div>
</div>
<div class="row mt-4">
<button class="col-1 btn btn-primary mb-2 mx-auto" onclick="explain()">Explain</button>
<button class="btn btn-primary mb-2 mx-auto" onclick="explain()">Explain</button>
</div>
</div>

View File

@ -230,6 +230,22 @@ function processAnswerKey() {
}
}
function processAnswerKeyDown(evt) {
if (evt.keyCode == 32) {
var options = getOptions();
if (options.use_voice) {
window.speechSynthesis.cancel();
textToSpeech(window.questionData.givenWordAsKanji, evt.shiftKey);
evt.preventDefault();
}
}
}
function validQuestion(entry, forms, transformation, options) {
var valid = true;
@ -341,21 +357,25 @@ function generateQuestion() {
var kanaForms = forms["hiragana"];
var furiganaForms = forms["furigana"];
var givenWord;
var candidates;
if (options["kana"]) {
givenWord = kanaForms[from_form].randomElement();
candidates = kanaForms[from_form];
} else {
givenWord = wordWithFurigana(furiganaForms[from_form]).randomElement();
candidates = wordWithFurigana(furiganaForms[from_form]);
}
var candidateIndex = Math.floor(Math.random() * candidates.length);
var givenWord = candidates[candidateIndex];
var givenWordAsKanji = kanjiForms[from_form][candidateIndex];
var thisQuestionText = questionText[transformation.phrase];
// thisQuestionText = thisQuestionText[0].toUpperCase() + thisQuestionText.substring(1);
var questionFirstHalf = thisQuestionText;
var questionSecondHalf = givenWord;
var question = questionFirstHalf.replace("the following", questionSecondHalf);
var answer = kanjiForms[to_form];
@ -368,7 +388,12 @@ function generateQuestion() {
}
$('#questionFirstHalf').html(questionFirstHalf);
$('#questionSecondHalf').html(questionSecondHalf);
if (options.use_voice) {
$('#questionSecondHalf').html("<div id='speechSpace'><i>Press Space for word</i><br><div class='halfSpeed'>Use Shift key for half speed</div></div>");
} else {
$('#questionSecondHalf').html(questionSecondHalf);
}
window.questionData = {
entry: entry,
@ -378,6 +403,7 @@ function generateQuestion() {
answer2: answer2,
answerWithFurigana: answerWithFurigana,
givenWord: givenWord,
givenWordAsKanji: givenWordAsKanji,
};
// Construct the explanation page.
@ -453,6 +479,7 @@ function generateQuestion() {
$('#answer').focus();
$('#answer').on('input', processAnswerKey);
$('#answer').on('keydown', processAnswerKeyDown);
}
function processAnswer() {
@ -597,12 +624,22 @@ function showSplash() {
}
function startQuiz() {
var options = getOptions();
const voiceSelectError = document.querySelector('#voiceSelectError');
if (options.use_voice && !getVoiceConfig()) {
voiceSelectError.style.display = "block";
return;
} else {
voiceSelectError.style.display = "none";
}
$('#splash').hide();
$('#quizSection').show();
$('#scoreSection').hide();
var options = getOptions();
if (options.furigana_always) {
$('body').addClass("furiganaAlways");
} else {
@ -621,6 +658,98 @@ function endQuiz() {
$('#backToStart').focus();
}
// Text to Speech
function loadVoiceList(callback) {
if (window.speechSynthesis.getVoices().length == 0) {
window.speechSynthesis.addEventListener('voiceschanged', function () {
if (callback) {
callback();
}
});
} else {
if (callback) {
callback();
}
}
}
function populateVoiceList() {
loadVoiceList(function () {
var voiceSelect = document.querySelector("#voice_select");
voiceSelect.innerHTML = "<option>Select voice...</option>" +
window.speechSynthesis.getVoices().map(function (voice) { return "<option>" + voice.name + "</option>" }).join("");
var currentVoice = getCurrentVoice();
if (currentVoice) {
voiceSelect.value = currentVoice;
}
});
}
function getVoiceConfig() {
return JSON.parse(localStorage.getItem("voiceConfig"));
}
function setVoiceConfig(config) {
localStorage.setItem("voiceConfig", JSON.stringify(config));
}
function getCurrentVoice() {
const voiceConfig = getVoiceConfig();
if (voiceConfig) {
return voiceConfig.voice;
}
}
function textToSpeech(text, slowMode, callback) {
loadVoiceList(function () {
const availableVoices = window.speechSynthesis.getVoices();
const voiceConfig = getVoiceConfig();
const currentVoice = voiceConfig.voice;
var voice = '';
for (var i = 0; i < availableVoices.length; i++) {
if (availableVoices[i].name == currentVoice) {
voice = availableVoices[i];
break;
}
}
if (voice === '') {
voice = availableVoices[0];
}
// new SpeechSynthesisUtterance object
var utter = new SpeechSynthesisUtterance();
utter.rate = slowMode ? voiceConfig.rate * 0.5 : voiceConfig.rate;;
utter.pitch = voiceConfig.pitch;
utter.text = text;
utter.voice = voice;
// event after text has been spoken
utter.onend = function () {
if (callback) {
callback(undefined);
}
}
// speak
window.speechSynthesis.speak(utter);
});
}
function arrayDifference(a, b) {
// From http://stackoverflow.com/a/1723220
return a.filter(function (x) { return b.indexOf(x) < 0 });
@ -803,6 +932,31 @@ function updateOptionSummary() {
$("#questionCount").text(applicable);
}
function updateVoiceSelect() {
const options = getOptions();
const voice_select_options = document.querySelector("#voice_select_options");
if (options.use_voice) {
voice_select_options.style.display = "block";
} else {
voice_select_options.style.display = "none";
}
}
function updateVoiceSelection() {
const newSelection = document.querySelector("#voice_select").selectedOptions[0].text;
const voiceConfig = {
voice: document.querySelector("#voice_select").selectedOptions[0].text,
rate: 1,
pitch: 1
};
setVoiceConfig(voiceConfig);
}
function explain() {
$('#explanation').show();
$('#message').hide();
@ -814,7 +968,8 @@ function getOptions() {
var options = ["plain", "polite", "negative", "past", "te-form",
"progressive", "potential", "imperative", "passive", "causative",
"godan", "ichidan", "iku", "kuru", "suru", "i-adjective", "na-adjective",
"ii", "desire", "volitional", "trick", "kana", "furigana_always"];
"ii", "desire", "volitional", "trick", "kana", "furigana_always",
"use_voice"];
var selects = ["questionFocus"];
@ -833,6 +988,13 @@ function getOptions() {
$('window').ready(function () {
if (window.speechSynthesis) {
populateVoiceList();
$('#useVoiceSection').show();
$('input#use_voice').click(updateVoiceSelect);
$('select#voice_select').on('change', updateVoiceSelection);
}
calculateAllConjugations();
calculateTransitions();