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 {
|
||||
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 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>
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user