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 { .btn-primary.focus, .btn-primary:focus {
box-shadow: 0 0 0 .2rem #0060c080; 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> </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="container options">
<div class="row"> <div class="row">
@ -143,6 +149,16 @@
<div class="text-center mb-4">Question pool size: <span id="questionCount">...</span></div> <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="row justify-content-center mt-3 ml-2 mr-2">
<div class="form-group"> <div class="form-group">
<div class="form-check"><input class="form-check-input" type="checkbox" id="trick" checked><label <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> 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 <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> 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>
</div> </div>
@ -218,7 +236,7 @@
<div class="col-12 text-center" id="correction"></div> <div class="col-12 text-center" id="correction"></div>
</div> </div>
<div class="row mt-4"> <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>
</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) { function validQuestion(entry, forms, transformation, options) {
var valid = true; var valid = true;
@ -341,21 +357,25 @@ function generateQuestion() {
var kanaForms = forms["hiragana"]; var kanaForms = forms["hiragana"];
var furiganaForms = forms["furigana"]; var furiganaForms = forms["furigana"];
var givenWord; var candidates;
if (options["kana"]) { if (options["kana"]) {
givenWord = kanaForms[from_form].randomElement(); candidates = kanaForms[from_form];
} else { } 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]; var thisQuestionText = questionText[transformation.phrase];
// thisQuestionText = thisQuestionText[0].toUpperCase() + thisQuestionText.substring(1); // thisQuestionText = thisQuestionText[0].toUpperCase() + thisQuestionText.substring(1);
var questionFirstHalf = thisQuestionText; var questionFirstHalf = thisQuestionText;
var questionSecondHalf = givenWord; var questionSecondHalf = givenWord;
var question = questionFirstHalf.replace("the following", questionSecondHalf); var question = questionFirstHalf.replace("the following", questionSecondHalf);
var answer = kanjiForms[to_form]; var answer = kanjiForms[to_form];
@ -368,7 +388,12 @@ function generateQuestion() {
} }
$('#questionFirstHalf').html(questionFirstHalf); $('#questionFirstHalf').html(questionFirstHalf);
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); $('#questionSecondHalf').html(questionSecondHalf);
}
window.questionData = { window.questionData = {
entry: entry, entry: entry,
@ -378,6 +403,7 @@ function generateQuestion() {
answer2: answer2, answer2: answer2,
answerWithFurigana: answerWithFurigana, answerWithFurigana: answerWithFurigana,
givenWord: givenWord, givenWord: givenWord,
givenWordAsKanji: givenWordAsKanji,
}; };
// Construct the explanation page. // Construct the explanation page.
@ -453,6 +479,7 @@ function generateQuestion() {
$('#answer').focus(); $('#answer').focus();
$('#answer').on('input', processAnswerKey); $('#answer').on('input', processAnswerKey);
$('#answer').on('keydown', processAnswerKeyDown);
} }
function processAnswer() { function processAnswer() {
@ -597,12 +624,22 @@ function showSplash() {
} }
function startQuiz() { 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(); $('#splash').hide();
$('#quizSection').show(); $('#quizSection').show();
$('#scoreSection').hide(); $('#scoreSection').hide();
var options = getOptions();
if (options.furigana_always) { if (options.furigana_always) {
$('body').addClass("furiganaAlways"); $('body').addClass("furiganaAlways");
} else { } else {
@ -621,6 +658,98 @@ function endQuiz() {
$('#backToStart').focus(); $('#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) { function arrayDifference(a, b) {
// From http://stackoverflow.com/a/1723220 // From http://stackoverflow.com/a/1723220
return a.filter(function (x) { return b.indexOf(x) < 0 }); return a.filter(function (x) { return b.indexOf(x) < 0 });
@ -803,6 +932,31 @@ function updateOptionSummary() {
$("#questionCount").text(applicable); $("#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() { function explain() {
$('#explanation').show(); $('#explanation').show();
$('#message').hide(); $('#message').hide();
@ -814,7 +968,8 @@ function getOptions() {
var options = ["plain", "polite", "negative", "past", "te-form", var options = ["plain", "polite", "negative", "past", "te-form",
"progressive", "potential", "imperative", "passive", "causative", "progressive", "potential", "imperative", "passive", "causative",
"godan", "ichidan", "iku", "kuru", "suru", "i-adjective", "na-adjective", "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"]; var selects = ["questionFocus"];
@ -833,6 +988,13 @@ function getOptions() {
$('window').ready(function () { $('window').ready(function () {
if (window.speechSynthesis) {
populateVoiceList();
$('#useVoiceSection').show();
$('input#use_voice').click(updateVoiceSelect);
$('select#voice_select').on('change', updateVoiceSelection);
}
calculateAllConjugations(); calculateAllConjugations();
calculateTransitions(); calculateTransitions();