Skip to content

Instantly share code, notes, and snippets.

@RascalTwo
Created June 11, 2022 17:25
Show Gist options
  • Save RascalTwo/52371ff5e8bbfae7148ee94199922250 to your computer and use it in GitHub Desktop.
Save RascalTwo/52371ff5e8bbfae7148ee94199922250 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Audio Anki
// @description Interact with Anki cards with your voice and ears, having the card automatically read aloud to you, then giving you the chance to speak your answer, and highlighting words you spoke that were in the card answer.
// @match https://ankiuser.net/study/
// @grant none
// @version 1.0
// @author Rascal_Two
// ==/UserScript==
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function waitFor(selector) {
while (!document.querySelector(selector)) await delay(100);
return document.querySelector(selector);
}
function roundReady() {
const questionPrompt = !!document.querySelector('#qa');
const answerButton = !!document.querySelector('#ansbuta');
return questionPrompt && answerButton && study && study.currentCard
}
myButton = null;
recognition = null;
listening = false;
parts = [];
function toggleRecording() {
setTimeout(() => {
myButton.textContent = listening ? 'Stop Recording' : 'Start Recording';
}, 100);
if (!recognition) {
const fullText = htmlToNode(study.currentCard.answer)
const fullWords = fullText.replace(/[!.,:?]/g, '').split(/\s+/)
.filter(word => word && word.length >= 2)
.map(word => word.toLowerCase())
.filter((word, i, array) => array.indexOf(word) === i)
.sort((a, b) => a.length - b.length)
recognition = new webkitSpeechRecognition();
const speechRecognitionList = new webkitSpeechGrammarList();
speechRecognitionList.addFromString(`#JSGF V1.0; grammar colors; public <rascaltwofulltext> = ${fullWords.join(' | ')} ;`, 1);
recognition.grammars = speechRecognitionList;
recognition.continuous = true;
recognition.lang = 'en-US';
recognition.interimResults = false;
recognition.maxAlternatives = 1;
recognition.start();
parts = [];
recognition.onresult = (event) => {
const result = event.results[event.resultIndex][0].transcript;
document.querySelector('#r2-transcript').appendChild(document.createTextNode(result + '.'));
parts.push(result);
}
listening = true;
return;
}
if (listening) recognition.stop();
else recognition.start();
listening = !listening;
}
function htmlToNode(html) {
const node = document.createElement('div');
node.innerHTML = html;
document.body.appendChild(node);
const text = node.innerText;
node.remove();
return text;
}
function renderTranscript() {
const transcript = document.createElement('div');
transcript.id = 'r2-transcript';
transcript.style.padding = '1rem';
transcript.style.border = '1px black dashed';
transcript.style.margin = 'auto';
const qaChildNodes = [...document.querySelector('#qa').childNodes]
const answerIndex = qaChildNodes.indexOf(document.querySelector('#answer'))
if (answerIndex !== -1) {
const answerElements = qaChildNodes.slice(answerIndex + 1).filter(node => node.textContent)
const spokenWords = parts.join('\n').replace(/[!.,:?]/g, '').split(/\s+/)
.filter(word => word && word.length >= 2)
.map(word => word.toLowerCase())
.filter((word, i, array) => array.indexOf(word) === i)
.sort((a, b) => b.length - a.length)
for (const node of answerElements) {
let changedHTML = node.nodeValue;
let coloringIndexes = []
for (const word of spokenWords) {
const match = changedHTML.match(new RegExp(word, 'i'))
if (match) coloringIndexes.push({ start: match.index, end: match.index + word.length, word })
}
coloringIndexes.sort((a, b) => a.start - b.start);
for (let i = 1; i < coloringIndexes.length; i++) {
const last = coloringIndexes[i - 1]
const current = coloringIndexes[i]
if (last.start === current.start) {
last.end = Math.max(last.end, current.end)
coloringIndexes.splice(i, 1)
i--;
} else if (last.end >= current.start) {
last.end = current.end
coloringIndexes.splice(i, 1)
i--;
}
}
for (let i = coloringIndexes.length - 1; i >= 0; i--) {
const { start, end } = coloringIndexes[i];
const changed = `<span style="background-color: lime;">${changedHTML.slice(start, end)}</span>`
changedHTML = changedHTML.slice(0, start) + changed + changedHTML.slice(end);
}
if (node.nodeValue === changedHTML) continue;
const div = document.createElement('div');
div.innerHTML = changedHTML;
document.querySelector('#qa').replaceChild(div, node)
}
}
document.querySelector('#qa').appendChild(transcript);
for (const part of parts) {
transcript.appendChild(document.createTextNode(part + '.'));
}
parts = []
document.querySelectorAll('#easebuts button').forEach(btn => btn.addEventListener('click', attach))
}
speakAgain = null;
function toggleSpeaking(force) {
const shouldStop = speakAgain.textContent === 'Stop'
speechSynthesis.cancel()
if (shouldStop && !force) return;
const utterance = new SpeechSynthesisUtterance(htmlToNode(study.currentCard.question))
speechSynthesis.speak(utterance);
utterance.addEventListener('start', () => speakAgain.textContent = 'Stop')
utterance.addEventListener('end', () => speakAgain.textContent = 'Start');
}
async function startRound() {
if (recognition){
recognition.onresult = () => undefined;
recognition = null;
listening = false;
}
if (!document.querySelector('#r2-my-button')) {
myButton = document.createElement('button');
myButton.id = 'r2-my-button';
myButton.className = 'btn btn-primary btn-lg';
myButton.textContent = 'Start Recording';
myButton.addEventListener('click', toggleRecording);
document.querySelector('#ansbuta').parentNode.appendChild(myButton);
}
if (!document.querySelector('#r2-speak-again')) {
speakAgain = document.createElement('button');
speakAgain.id = 'r2-speak-again';
speakAgain.className = 'btn btn-primary btn-lg';
speakAgain.textContent = 'Speak';
speakAgain.addEventListener('click', () => toggleSpeaking());
document.querySelector('#qa').prepend(speakAgain);
}
toggleSpeaking(true)
renderTranscript()
document.querySelector('#ansbuta').addEventListener('click', () => {
if (listening) toggleRecording()
return renderTranscript()
}, { once: true })
}
async function attach() {
while (!roundReady()) {
await delay(1000);
}
startRound();
}
attach().catch(console.error)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment