mirror of
https://github.com/ZetaKebab/japanese-conjugation-drill.git
synced 2025-01-14 22:08:44 +00:00
Added experimental speech support.
This commit is contained in:
parent
ad88fb3642
commit
8daf3d81da
@ -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;
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user