您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically generates Audio for items you're learning. Configure in left panel.
// ==UserScript== // @name Memrise Audio Provider // @description Automatically generates Audio for items you're learning. Configure in left panel. // @match https://*.memrise.com/course/*/garden/* // @match https://*.memrise.com/garden/review/* // @run-at document-end // @version 1.1.4 // @grant none // @namespace https://gf.qytechs.cn/users/213706 // ==/UserScript== // Based on https://github.com/cooljingle/memrise-audio-provider function main() { // This script has three different methos to generate audio // Speech Synthesis API const SPEECHSYNTHESIS_LANG = { "German": "de-DE", "English": "en-GB", "Spanish (Mexico)": "es-ES", "Spanish (Spain)": "es-ES", "French": "fr-FR", "Hindi": "hi-IN", "Indonesian": "id-ID", "Italian": "it-IT", "Japanese": "ja-JP", "Kanji": "ja-JP", "Korean": "ko-KR", "Dutch": "nl-NL", "Polish": "pl-PL", "Portuguese (Brazil)": "pt-BR", "Russian": "ru-RU", "Chinese (Simplified)": "zh-CN", "Cantonese": "zh-HK", "Chinese (Traditional)": "zh-TW" }; // Google TTS API const GOOGLETTS_LANG = { "Afrikaans": "af", "Albanian": "sq", "Arabic": "ar", "Armenian": "hy", "Bengali": "bn", "Bosnian": "bs", "Catalan": "ca", "Chinese (Simplified)": "zh-CN", "Chinese (Traditional)": "zh-TW", "Croatian": "hr", "Czech": "cs", "Danish": "da", "Dutch": "nl", "English": "en", "Esperanto": "eo", "Finnish": "fi", "French": "fr", "German": "de", "Greek": "el", "Hindi": "hi", "Hungarian": "hu", "Icelandic": "is", "Indonesian": "id", "Italian": "it", "Japanese": "ja", "Kanji": "ja", "Khmer": "km", "Korean": "ko", "Latin": "la", "Latvian": "lv", "Macedonian": "mk", "Nepali": "ne", "Norwegian": "no", "Polish": "pl", "Portuguese (Brazil)": "pt-BR", "Portuguese (Portugal)": "pt-PT", "Romanian": "ro", "Russian": "ru", "Serbian": "sr", "Sinhalese": "si", "Slovak": "sk", "Spanish (Mexico)": "es", "Spanish (Spain)": "es", "Swahili": "sw", "Swedish": "sv", "Tamil": "ta", "Thai": "th", "Turkish": "tr", "Ukrainian": "uk", "Vietnamese": "vi", "Welsh": "cy" }; // Voice RSS API const VOICERSS_LANG = { "Catalan": "ca-es", "Chinese (Simplified)": "zh-cn", "Chinese (Traditional)": "zh-tw", "Danish": "da-dk", "Dutch": "nl-nl", "English": "en-gb", "fi-fi": "fi", "French": "fr-fr", "German": "de-de", "Italian": "it-it", "Japanese": "ja-jp", "Kanji": "ja-jp", "Korean": "ko-kr", "Norwegian": "nb-no", "Polish": "pl-pl", "Portuguese (Brazil)": "pt-br", "Portuguese (Portugal)": "pt-pt", "Russian": "ru-ru", "Spanish (Mexico)": "es-es", "Spanish (Spain)": "es-es", "Swedish": "sv-se" }; const LOCALSTORAGE_ID = "memrise-audio-provider-storagev2", LOCALSTORAGE_VOICERSSID = "memrise-audio-provider-voicerss", LOCALSTORAGE_OVERRIDEID = "memrise-audio-provider-override-all", NONE = "-- None"; var AudioProvider = { /** * Entrypoint */ init: function(){ // Check current referer this.referrerState = ""; this.requestCount = 0; this.watchNetwork(); // Which API to use ? this.speechSynthesis = window.speechSynthesis && new window.SpeechSynthesisUtterance(); this.speechSynthesisPlaying = false; this.canSpeechSynthesize = true; this.canGoogleTts = true; this.canVoiceRss = true; // Current state this.language = null; // current language (label) this.courseId = null; // current course id this.wordColumn = null; // name of field containing word this.currentItem = null; // current item to refresh current word when changing settings this.currentWord = ""; // current word this.cachedAudioElements = []; // cached audio DOMElements // Add Meta to get Google TTS working var meta = document.createElement('meta'); meta.name = "referrer"; meta.content = "origin"; document.getElementsByTagName('head')[0].appendChild(meta); // Add AudioProvider to window this.add_settings(); if(MEMRISE.garden.session) { this.override_memrise(); } else if(MEMRISE.garden._events && MEMRISE.garden._events.start) { MEMRISE.garden._events.start.push(this.override_memrise.bind(this)); } else { setTimeout(function(){ this.override_memrise(); }.bind(this), 1000); } }, //+------------------------------------------------------ //| //| SETTINGS //| //+------------------------------------------------------ /** * Add Settings block to left area */ add_settings: function(){ // Retrieve saved settings this.savedChoices = JSON.parse(localStorage.getItem(LOCALSTORAGE_ID)) || {}; this.voiceRssKey = localStorage.getItem(LOCALSTORAGE_VOICERSSID) || ""; this.overrideAllAudio = localStorage.getItem(LOCALSTORAGE_OVERRIDEID) === "true"; if(!this.voiceRssKey) { this.canVoiceRss = false; } // Add "audio provider" settings to left area var div = document.createElement('div'), linkHtml = ` <p id='audio-provider-link'>Audio Provider</p> <div id='audio-provider-box' style='display:none'> <div style='display:table; padding: 5px; background: rgba(255,255,255,0.6);'> <em style='font-size:85%; white-space: nowrap;'>Column to use:</em><br> <select id='audio-provider-options'></select> <em style='font-size:85%; white-space: nowrap;'>Override all audio:</em> <input id='audio-provider-override' type="checkbox" ${this.overrideAllAudio ? 'checked="checked"': ''}> <em style='font-size:85%; white-space: nowrap;'>Voice RSS key:</em><br> <input id='audio-provider-voicerss' type='text' placeholder='enter Voice RSS key' value="${this.voiceRssKey.replace(/"/g, '\"')}" style='width: 150px; padding: 8px;'> </div> </div>`; div.innerHTML = linkHtml; document.getElementById('left-area').appendChild(div); // Show/hide settings document.getElementById('audio-provider-link').addEventListener('click', function () { var box = document.getElementById('audio-provider-box'); box.style.display = (box.style.display == "none" ? "block" : "none"); }); // User changes settings document.getElementById('audio-provider-voicerss').addEventListener('change', function () { localStorage.setItem(LOCALSTORAGE_VOICERSSID, this.value); }); document.getElementById('audio-provider-override').addEventListener('change', function () { var checked = this.checked; AudioProvider.overrideAllAudio = checked; localStorage.setItem(LOCALSTORAGE_OVERRIDEID, checked); }); document.getElementById('audio-provider-options').addEventListener('change', function () { var wordColumn = this.value; AudioProvider.wordColumn = wordColumn; AudioProvider.savedChoices[AudioProvider.courseId] = wordColumn; localStorage.setItem(LOCALSTORAGE_ID, JSON.stringify(AudioProvider.savedChoices)); AudioProvider.updateCurrentWord(); }); }, /** * Set speechSynthesis language * Called by MEMRISE when ready (cf override_memrise) */ init_speechSynthesis: function(){ if(!this.speechSynthesis || !this.canSpeechSynthesize) { return; } var langCode = SPEECHSYNTHESIS_LANG[this.language]; this.speechSynthesis.lang = langCode || ""; this.speechSynthesis.voice = speechSynthesis.getVoices().filter(function (voice) { return voice.lang === langCode; })[0]; this.canSpeechSynthesize = !!(this.speechSynthesis.lang && this.speechSynthesis.voice); }, /** * @param object context */ set_content: function(context) { if (context.template == "end_of_session" || context.template == "speed-count-down") { return; } // Which field to use to generate audio ? var courseId = context.course_id || MEMRISE.garden.session_params.course_id || MEMRISE.garden.session_data.learnables_to_courses[context.learnable.learnable_id]; if (this.courseId != courseId) { this.courseId = courseId; this.wordColumn = this.savedChoices[courseId] || (context.learnable.item.kind == "text" ? context.learnable.item.label : (context.learnable.definition.kind == "text" ? context.learnable.definition.label : NONE)); this.editAudioOptions(context.learnable); } // Set current word if (!this.canSpeechSynthesize && !this.canGoogleTts && !this.canVoiceRss){ document.getElementById('audio-provider-link').style.display = "none"; log("could not find a way to generate audio for language " + language); return; } this.currentItem = { item: context.learnable.item, definition: context.learnable.definition, audio : (context.presentationData || context.testData).audio.value }; this.updateCurrentWord(context); }, /** * Populate settings content * Called by MEMRISE whenever we change current course (cf make_box > set_content) * * @param object word */ editAudioOptions: function(word) { var html = `<option value="${NONE}">${NONE}</option>`; if(word.definition.kind == "text") { html += `<option value="${word.definition.label}">${word.definition.label}</option>`; } if(word.item.kind == "text") { html += `<option value="${word.item.label}">${word.item.label}</option>`; } var options = document.getElementById('audio-provider-options'); options.innerHTML = html; options.value = this.wordColumn; }, /** * Update currentWord * Called by MEMRISE when rendering word (cf make_box > set_content) * And by AudioProvider when updating wordColumn */ updateCurrentWord: function() { var item = this.currentItem; if(!item) { return; } // Should we generate audio ? var handleAudio = this.overrideAllAudio || (_.isArray(item.audio) ? item.audio[0] : item.audio).normal === "AUDIO_PROVIDER"; if(!handleAudio || this.wordColumn === NONE) { this.currentWord = ""; return; } // Get word this.currentWord = _.find([item.definition, item.item], x => x.label === this.wordColumn).value; // GoogleTTS: preload as we change referrer header while loading (we don't want to conflict with memrise calls) if (!this.canSpeechSynthesize && this.canGoogleTts) { this.getGoogleTtsElement(this.currentWord); } }, //+------------------------------------------------------ //| //| OVERRIDE MEMRISE //| //+------------------------------------------------------ /** * Override Memrise functions with our owns */ override_memrise: function(){ // Override MEMRISE's functions this.cached = { make_box : MEMRISE.garden.session.make_box, fixMediaUrl : MEMRISE.renderer.fixMediaUrl, play : MEMRISE.audioPlayer.play }; MEMRISE.garden.session.make_box = this.make_box; MEMRISE.renderer.fixMediaUrl = this.fixMediaUrl; MEMRISE.audioPlayer.play = this.play; // Populate audio this.language = MEMRISE.garden.session.category.name; this.init_speechSynthesis(); this.populateScreenAudios(); // Manually call make_box if MEMRISE already loaded content if(MEMRISE.garden.box) { this.set_content(MEMRISE.garden.box); // Add audio player to window var columns = document.querySelector('.columns'); // Presentation if(columns) { if(!columns.querySelector('.first-audio')) { var div = document.createElement('div'); div.setAttribute('class', 'row column first-audio'); div.innerHTML = '<div class="row-value"><a class="audio-player audio-player-hover" href=""></a></div>'; columns.appendChild(div); } // Multi-choice/typing/tapping } else { var audio = document.querySelector('.hidden-audio'); if(audio && audio.children.length == 0) { audio.innerHTML = '<a class="audio-player audio-player-hover" href=""></a>'; } } } }, populateScreenAudios: function() { var learnables = MEMRISE.garden.learnables || _.indexBy(MEMRISE.garden.session_data.learnables, 'learnable_id'); _.each(learnables, function(v, k) { var learnableScreens = (MEMRISE.garden.screens || MEMRISE.garden.session_data.screens)[k], screenMap = MEMRISE.garden.screen_template_map[k]; _.each([learnableScreens, screenMap], screens => { _.each(screens, s => { s = _.isArray(s) ? s[0] : s; var hasAudio = s.audio && s.audio.value && s.audio.value.length; if(!hasAudio){ s.audio = { alternatives: [], direction: "target", kind: "audio", label: "Audio", style: [], value: [{ normal: "AUDIO_PROVIDER", slow: "AUDIO_PROVIDER" }] }; } // end if }); }); }); }, /** * @return string */ make_box: function(){ var context = AudioProvider.cached.make_box.apply(this, arguments); AudioProvider.set_content(context); return context; }, /** * @return string */ fixMediaUrl: function () { if (AudioProvider.overrideAllAudio || arguments[0] === "AUDIO_PROVIDER" || (_.isArray(arguments[0]) && arguments[0][0] === "AUDIO_PROVIDER")) { return ""; } else { return AudioProvider.cached.fixMediaUrl.apply(this, arguments); } }, /** * Play audio * Generates automatically if necessary */ play: function () { if(document.body.classList.contains("audio-muted")) { return; } var shouldGenerateAudio = (arguments[0].url === ""); if (shouldGenerateAudio) { AudioProvider.playGeneratedAudio(AudioProvider.currentWord); } else { AudioProvider.cached.play.apply(this, arguments); } }, //+------------------------------------------------------ //| //| GENERATE AUDIO //| //+------------------------------------------------------ /** * Generates audio for given word * With the first API that is enabled */ playGeneratedAudio: function(word) { if (!word) { return; } if (this.canSpeechSynthesize) { this.playSpeechSynthesisAudio(word); } else if (this.canGoogleTts) { this.playGoogleTtsAudio(word); } else if (this.canVoiceRss) { this.playVoiceRssAudio(word); } else { log("no playable sources found"); } }, /** * Generate audio with SpeechSynthesis API * for the given word and plays it * * @param string word */ playSpeechSynthesisAudio: function(word) { if(this.speechSynthesis.text === word && this.speechSynthesisPlaying){ return; } log("generating speechSynthesis audio for word: " + word); this.speechSynthesis.text = word; window.speechSynthesis.speak(this.speechSynthesis); this.speechSynthesisPlaying = true; this.speechSynthesis.onend = function (event) { this.speechSynthesisPlaying = false; // Firefox utterances don't play more than once if (navigator.userAgent.search("Firefox") > -1) { var lang = this.speechSynthesis.lang, voice = this.speechSynthesis.voice, test = this.speechSynthesis.text; this.speechSynthesis = new window.SpeechSynthesisUtterance(); this.speechSynthesis.lang = lang; this.speechSynthesis.voice = voice; this.speechSynthesis.text = text; } }.bind(this); }, /** * Generate audio with Google TTS API * for the given word and plays it * * @param string word */ playGoogleTtsAudio: function(word) { this.getGoogleTtsElement(word, true); }, getGoogleTtsElement: function(word, play) { // Check language is recognized by Google var languageCode = GOOGLETTS_LANG[this.language]; if (!languageCode) { return; } // Is audio cached ? var source = "google tts", cachedElement = this.getCachedElement(source, word); if (cachedElement) { play && cachedElement.play(); return; } // If network is busy: delay TTS request if (this.isNetworkBusy()) { log("network busy - delaying google tts load"); setTimeout(function(){ AudioProvider.getGoogleTtsElement(word, play); }, 300); return; } // Generate audio log("generating google tts link for word: " + word); var audioElement = this.makeAudioElement({ label : source, word : word, url : this.getGoogleTtsUrl(languageCode, word), onError: function (e) { if(referrerState === "origin") { console.log("referrer header was set prematurely"); this.removeCachedElement(source, word); } else { this.canGoogleTts = false; this.setReferrerOrigin(); } this.playGeneratedAudio(word); }.bind(this) }); this.setCachedElement(source, word, audioElement); if (navigator.userAgent.search("Firefox") > -1) { audioElement.addEventListener('loadstart', this.setReferrerNoReferrer.bind(this)); } else { this.setReferrerNoReferrer(); } audioElement.addEventListener('loadedmetadata', this.setReferrerOrigin.bind(this)); if(play) { audioElement.play(); } }, /** * Returns Google TTS url * for the given word and language * * @return string */ getGoogleTtsUrl: function(languageCode, word) { // Extra params help to stop google from complaining about too many requests return `https://translate.google.com/translate_tts?ie=UTF-8&tl=${languageCode}&client=tw-ob&q=${encodeURIComponent(word)}&tk=${Math.floor(Math.random() * 1000000)}`; }, /** * Generate audio with Voice RSS API * for the given word and plays it * * @param string word */ playVoiceRssAudio: function(word) { // Check language is recognized by Voice RSS var languageCode = VOICERSS_LANG[language]; if(!languageCode){ return; } // Is audio cached ? var source = "voice rss", cachedElement = this.getCachedElement(source, word); if (cachedElement) { cachedElement.play(); return; } // Generate audio log("generating voice rss link for word: " + word); var audioElement = this.makeAudioElement({ label : source, word : word, url : this.getVoiceRssUrl(languageCode, word), onError: function (e) { this.canVoiceRss = false; this.playGeneratedAudio(word); }.bind(this) }); this.setCachedElement(source, word, audioElement); audioElement.play(); }, /** * Returns Voice RRS url * for the given word and language * * @return string */ getVoiceRssUrl: function(languageCode, word) { return `https://api.voicerss.org/?key=${this.voiceRssKey}&src=${encodeURIComponent(word)}&hl=${languageCode}&f=48khz_16bit_stereo`; }, /** * Creates an audio DOMElement * @param object options - {source: string, word: string, url: string, onError: function} * @return DOMElement */ makeAudioElement: function({source, word, url, onError}) { var audioElement = document.createElement('audio'); audioElement.setAttribute('src', url); audioElement.addEventListener('error', function(e) { log(source + " failed"); console.log(e); onError(e); }); return audioElement; }, //+------------------------------------------------------ //| //| CACHE FUNCTIONS //| //+------------------------------------------------------ getCachedElement: function(source, word) { var cachedElem = this.cachedAudioElements.find((obj) => { return obj.source === source && obj.word === word; }); return cachedElem && cachedElem.element; }, removeCachedElement: function(source, word) { _.remove(this.cachedAudioElements, (e) => { return e.source === source && e.word === word }); }, setCachedElement: function(source, word, element) { this.cachedAudioElements.push({source, word, element}); }, //+------------------------------------------------------ //| //| REFERER FUNCTIONS //| //+------------------------------------------------------ setReferrerOrigin: function() { document.getElementsByName("referrer")[0].setAttribute("content", "origin"); this.referrerState = "origin"; }, setReferrerNoReferrer: function() { document.getElementsByName("referrer")[0].setAttribute("content", "no-referrer"); this.referrerState = "no-referrer"; }, isNetworkBusy: function() { return this.requestCount > 0; }, watchNetwork: function() { $(document).ajaxSend(function (e, xhr, settings) { this.requestCount++; if(this.referrerState === "no-referrer") { this.setReferrerOrigin(); } xhr.always(function() { this.requestCount--; }.bind(this)); }.bind(this)); } }; function log(message) { console.log("Audio Provider: " + message); } window.addEventListener('load', function(){ AudioProvider.init(); }, false); } // Inject JS directly in page to prevent limitations of access var script = document.createElement('script'); script.setAttribute("type", "application/javascript"); script.appendChild(document.createTextNode('('+ main +')();')); document.body.appendChild(script);
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址