diff --git a/ext/css/display.css b/ext/css/display.css index bf15b2a34b..24b7134da3 100644 --- a/ext/css/display.css +++ b/ext/css/display.css @@ -1995,6 +1995,13 @@ button.footer-notification-close-button { .popup-menu-item-group[data-is-primary-card-audio=true]>.popup-menu-item-audio-button~.popup-menu-item-set-primary-audio-button:focus .popup-menu-item-icon { opacity: 1; } +.popup-menu-item-group[data-is-sentence-card-audio=true]>.popup-menu-item-audio-button~.popup-menu-item-set-primary-audio-button .popup-menu-item-icon, +.popup-menu-item-group[data-is-sentence-card-audio=true]>.popup-menu-item-audio-button~.popup-menu-item-set-primary-audio-button:hover .popup-menu-item-icon, +.popup-menu-item-group[data-is-sentence-card-audio=true]>.popup-menu-item-audio-button~.popup-menu-item-set-primary-audio-button:active .popup-menu-item-icon, +.popup-menu-item-group[data-is-sentence-card-audio=true]>.popup-menu-item-audio-button~.popup-menu-item-set-primary-audio-button:focus .popup-menu-item-icon { + opacity: 1; + background-color: #6495ed; /* cornflowerblue */ +} /* Anki errors */ diff --git a/ext/data/schemas/custom-audio-list-schema.json b/ext/data/schemas/custom-audio-list-schema.json index 885ad08761..ff1c007675 100644 --- a/ext/data/schemas/custom-audio-list-schema.json +++ b/ext/data/schemas/custom-audio-list-schema.json @@ -26,6 +26,9 @@ }, "url": { "type": "string" + }, + "sentence": { + "type": "string" } } } diff --git a/ext/data/templates/anki-field-templates-upgrade-v72.handlebars b/ext/data/templates/anki-field-templates-upgrade-v72.handlebars new file mode 100644 index 0000000000..f7a7d48cba --- /dev/null +++ b/ext/data/templates/anki-field-templates-upgrade-v72.handlebars @@ -0,0 +1,9 @@ +{{#*inline "sample-sentence-audio"}} + {{~#if (hasMedia "sampleSentenceAudio")~}} + [sound:{{getMedia "sampleSentenceAudio"}}] + {{~/if~}} +{{/inline}} + +{{#*inline "sample-sentence-text"}} + {{~#if (hasMedia "sampleSentenceText")}}{{{getMedia "sampleSentenceText"}}}{{/if~}} +{{/inline}} diff --git a/ext/data/templates/default-anki-field-templates.handlebars b/ext/data/templates/default-anki-field-templates.handlebars index e0c68db39e..cc164e3d05 100644 --- a/ext/data/templates/default-anki-field-templates.handlebars +++ b/ext/data/templates/default-anki-field-templates.handlebars @@ -36,6 +36,16 @@ {{~/if~}} {{/inline}} +{{#*inline "sample-sentence-audio"}} + {{~#if (hasMedia "sampleSentenceAudio")~}} + [sound:{{getMedia "sampleSentenceAudio"}}] + {{~/if~}} +{{/inline}} + +{{#*inline "sample-sentence-text"}} + {{~#if (hasMedia "sampleSentenceText")}}{{{getMedia "sampleSentenceText"}}}{{/if~}} +{{/inline}} + {{#*inline "character"}} {{~definition.character~}} {{/inline}} diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 3d94b50271..435005b5b0 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -730,12 +730,13 @@ export class Backend { } /** @type {import('api').ApiHandler<'injectAnkiNoteMedia'>} */ - async _onApiInjectAnkiNoteMedia({timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails}) { + async _onApiInjectAnkiNoteMedia({timestamp, definitionDetails, audioDetails, sampleSentenceAudioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails}) { return await this._injectAnkNoteMedia( this._anki, timestamp, definitionDetails, audioDetails, + sampleSentenceAudioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails, @@ -2261,16 +2262,19 @@ export class Backend { * @param {number} timestamp * @param {import('api').InjectAnkiNoteMediaDefinitionDetails} definitionDetails * @param {?import('api').InjectAnkiNoteMediaAudioDetails} audioDetails + * @param {?import('api').InjectAnkiNoteMediaSampleSentenceAudioDetails} sampleSentenceAudioDetails * @param {?import('api').InjectAnkiNoteMediaScreenshotDetails} screenshotDetails * @param {?import('api').InjectAnkiNoteMediaClipboardDetails} clipboardDetails * @param {import('api').InjectAnkiNoteMediaDictionaryMediaDetails[]} dictionaryMediaDetails * @returns {Promise>} */ - async _injectAnkNoteMedia(ankiConnect, timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails) { + async _injectAnkNoteMedia(ankiConnect, timestamp, definitionDetails, audioDetails, sampleSentenceAudioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails) { let screenshotFileName = null; let clipboardImageFileName = null; let clipboardText = null; let audioFileName = null; + let sampleSentenceAudioFileName = null; + let sampleSentenceText = null; const errors = []; try { @@ -2305,6 +2309,14 @@ export class Backend { errors.push(ExtensionError.serialize(e)); } + try { + if (sampleSentenceAudioDetails !== null) { + ({sampleSentenceAudioFileName, sampleSentenceText} = await this._injectAnkiNoteSampleSentenceAudio(ankiConnect, timestamp, definitionDetails, sampleSentenceAudioDetails)); + } + } catch (e) { + errors.push(ExtensionError.serialize(e)); + } + /** @type {import('api').InjectAnkiNoteDictionaryMediaResult[]} */ let dictionaryMedia; try { @@ -2323,6 +2335,8 @@ export class Backend { clipboardImageFileName, clipboardText, audioFileName, + sampleSentenceAudioFileName, + sampleSentenceText, dictionaryMedia, errors: errors, }; @@ -2368,6 +2382,54 @@ export class Backend { return await ankiConnect.storeMediaFile(fileName, data); } + /** + * @param {AnkiConnect} ankiConnect + * @param {number} timestamp + * @param {import('api').InjectAnkiNoteMediaDefinitionDetails} definitionDetails + * @param {import('api').InjectAnkiNoteMediaSampleSentenceAudioDetails} details + * @returns {Promise<{sampleSentenceAudioFileName: ?string, sampleSentenceText: ?string}>} + */ + async _injectAnkiNoteSampleSentenceAudio(ankiConnect, timestamp, definitionDetails, details) { + /** @type {{sampleSentenceAudioFileName: ?string, sampleSentenceText: ?string}} */ + const res = {sampleSentenceAudioFileName: null, sampleSentenceText: null}; + if (definitionDetails.type !== 'term') { return res; } + const {term, reading} = definitionDetails; + if (term.length === 0 && reading.length === 0) { return res; } + + const {sources, preferredAudioIndex, idleTimeout, languageSummary} = details.details; + let audio; + let text; + let data; + let contentType; + try { + ({audio, text} = await this._audioDownloader.downloadSampleSentenceAudio( + sources, + preferredAudioIndex, + term, + reading, + idleTimeout, + languageSummary, + details.downloadAudio, + )); + if (typeof text === 'string') { res.sampleSentenceText = text; } + if (!details.downloadAudio || audio === null) { return res; } + ({data, contentType} = audio); + } catch (e) { + const error = this._getAudioDownloadError(e); + if (error !== null) { throw error; } + // No audio + log.logGenericError(e, 'log'); + return res; + } + + let extension = contentType !== null ? getFileExtensionFromAudioMediaType(contentType) : null; + if (extension === null) { extension = '.mp3'; } + let fileName = generateAnkiNoteMediaFileName('yomitan_sentence_audio', extension, timestamp); + fileName = fileName.replace(/\]/g, ''); + res.sampleSentenceAudioFileName = await ankiConnect.storeMediaFile(fileName, data); + return res; + } + /** * @param {AnkiConnect} ankiConnect * @param {number} timestamp diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index e16e9a7171..413c5aeefa 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -125,13 +125,14 @@ export class API { * @param {import('api').ApiParam<'injectAnkiNoteMedia', 'timestamp'>} timestamp * @param {import('api').ApiParam<'injectAnkiNoteMedia', 'definitionDetails'>} definitionDetails * @param {import('api').ApiParam<'injectAnkiNoteMedia', 'audioDetails'>} audioDetails + * @param {import('api').ApiParam<'injectAnkiNoteMedia', 'sampleSentenceAudioDetails'>} sampleSentenceAudioDetails * @param {import('api').ApiParam<'injectAnkiNoteMedia', 'screenshotDetails'>} screenshotDetails * @param {import('api').ApiParam<'injectAnkiNoteMedia', 'clipboardDetails'>} clipboardDetails * @param {import('api').ApiParam<'injectAnkiNoteMedia', 'dictionaryMediaDetails'>} dictionaryMediaDetails * @returns {Promise>} */ - injectAnkiNoteMedia(timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails) { - return this._invoke('injectAnkiNoteMedia', {timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails}); + injectAnkiNoteMedia(timestamp, definitionDetails, audioDetails, sampleSentenceAudioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails) { + return this._invoke('injectAnkiNoteMedia', {timestamp, definitionDetails, audioDetails, sampleSentenceAudioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails}); } /** diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js index e456e4753c..1824a3ce9e 100644 --- a/ext/js/data/anki-note-builder.js +++ b/ext/js/data/anki-note-builder.js @@ -395,6 +395,8 @@ export class AnkiNoteBuilder { // Parse requirements let injectAudio = false; + let injectSampleSentenceAudio = false; + let injectSampleSentenceText = false; let injectScreenshot = false; let injectClipboardImage = false; let injectClipboardText = false; @@ -407,6 +409,8 @@ export class AnkiNoteBuilder { const {type} = requirement; switch (type) { case 'audio': injectAudio = true; break; + case 'sampleSentenceAudio': injectSampleSentenceAudio = true; break; + case 'sampleSentenceText': injectSampleSentenceText = true; break; case 'screenshot': injectScreenshot = true; break; case 'clipboardImage': injectClipboardImage = true; break; case 'clipboardText': injectClipboardText = true; break; @@ -430,6 +434,8 @@ export class AnkiNoteBuilder { const dictionaryEntryDetails = this.getDictionaryEntryDetailsForNote(dictionaryEntry); /** @type {?import('api').InjectAnkiNoteMediaAudioDetails} */ let audioDetails = null; + /** @type {?import('api').InjectAnkiNoteMediaSampleSentenceAudioDetails} */ + let sampleSentenceAudioDetails = null; /** @type {?import('api').InjectAnkiNoteMediaScreenshotDetails} */ let screenshotDetails = null; /** @type {import('api').InjectAnkiNoteMediaClipboardDetails} */ @@ -441,6 +447,16 @@ export class AnkiNoteBuilder { audioDetails = {sources, preferredAudioIndex, idleTimeout, languageSummary, enableDefaultAudioSources}; } } + if ((injectSampleSentenceAudio || injectSampleSentenceText) && dictionaryEntryDetails.type !== 'kanji') { + const sampleSentenceAudioOptions = mediaOptions.sampleSentenceAudio; + if (typeof sampleSentenceAudioOptions === 'object' && sampleSentenceAudioOptions !== null) { + const {sources, preferredAudioIndex, idleTimeout, languageSummary, enableDefaultAudioSources} = sampleSentenceAudioOptions; + sampleSentenceAudioDetails = { + details: {sources, preferredAudioIndex, idleTimeout, languageSummary, enableDefaultAudioSources}, + downloadAudio: injectSampleSentenceAudio, + }; + } + } if (injectScreenshot) { const screenshotOptions = mediaOptions.screenshot; if (typeof screenshotOptions === 'object' && screenshotOptions !== null) { @@ -465,11 +481,21 @@ export class AnkiNoteBuilder { timestamp, dictionaryEntryDetails, audioDetails, + sampleSentenceAudioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails, ); - const {audioFileName, screenshotFileName, clipboardImageFileName, clipboardText, dictionaryMedia: dictionaryMediaArray, errors} = injectedMedia; + const { + audioFileName, + sampleSentenceAudioFileName, + sampleSentenceText, + screenshotFileName, + clipboardImageFileName, + clipboardText, + dictionaryMedia: dictionaryMediaArray, + errors, + } = injectedMedia; const textFurigana = textFuriganaPromise !== null ? await textFuriganaPromise : []; // Format results @@ -486,6 +512,8 @@ export class AnkiNoteBuilder { } const media = { audio: (typeof audioFileName === 'string' ? {value: audioFileName} : void 0), + sampleSentenceAudio: (typeof sampleSentenceAudioFileName === 'string' ? {value: sampleSentenceAudioFileName} : void 0), + sampleSentenceText: (typeof sampleSentenceText === 'string' ? {value: sampleSentenceText} : void 0), screenshot: (typeof screenshotFileName === 'string' ? {value: screenshotFileName} : void 0), clipboardImage: (typeof clipboardImageFileName === 'string' ? {value: clipboardImageFileName} : void 0), clipboardText: (typeof clipboardText === 'string' ? {value: clipboardText} : void 0), diff --git a/ext/js/data/anki-template-util.js b/ext/js/data/anki-template-util.js index 1e79225018..f5c1e3a440 100644 --- a/ext/js/data/anki-template-util.js +++ b/ext/js/data/anki-template-util.js @@ -59,6 +59,8 @@ export function getStandardFieldMarkers(type, language = 'ja') { 'sentence', 'sentence-furigana', 'sentence-furigana-plain', + 'sample-sentence-audio', + 'sample-sentence-text', 'tags', 'url', ]; diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index 8557201995..cabc40bb68 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -583,6 +583,7 @@ export class OptionsUtil { this._updateVersion69, this._updateVersion70, this._updateVersion71, + this._updateVersion72, ]; /* eslint-enable @typescript-eslint/unbound-method */ if (typeof targetVersion === 'number' && targetVersion < result.length) { @@ -1795,6 +1796,15 @@ export class OptionsUtil { options.global.dataTransmissionConsentShown = false; } + /** + * - Added sample-sentence-audio and sample-sentence-text handlebars + * @type {import('options-util').UpdateFunction} + */ + async _updateVersion72(options) { + await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v72.handlebars'); + } + + /** * @param {string} url * @returns {Promise} diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js index 4f5a20699a..eded91abac 100644 --- a/ext/js/display/display-anki.js +++ b/ext/js/display/display-anki.js @@ -830,6 +830,8 @@ export class DisplayAnki { const {type} = requirement; switch (type) { case 'audio': + case 'sampleSentenceAudio': + case 'sampleSentenceText': case 'clipboardImage': break; default: @@ -1008,6 +1010,7 @@ export class DisplayAnki { const contentOrigin = this._display.getContentOrigin(); const details = this._ankiNoteBuilder.getDictionaryEntryDetailsForNote(dictionaryEntry); const audioDetails = this._getAnkiNoteMediaAudioDetails(details); + const sampleSentenceAudioDetails = this._getAnkiNoteMediaAudioDetails(details, true); const optionsContext = this._display.getOptionsContext(); const dictionaryStylesMap = this._ankiNoteBuilder.getDictionaryStylesMap(this._dictionaries); @@ -1024,6 +1027,7 @@ export class DisplayAnki { compactTags: this._compactTags, mediaOptions: { audio: audioDetails, + sampleSentenceAudio: sampleSentenceAudioDetails, screenshot: { format: this._screenshotFormat, quality: this._screenshotQuality, @@ -1063,11 +1067,12 @@ export class DisplayAnki { /** * @param {import('api').InjectAnkiNoteMediaDefinitionDetails} details + * @param {boolean} isSentence * @returns {?import('anki-note-builder').AudioMediaOptions} */ - _getAnkiNoteMediaAudioDetails(details) { + _getAnkiNoteMediaAudioDetails(details, isSentence = false) { if (details.type !== 'term') { return null; } - const {sources, preferredAudioIndex, enableDefaultAudioSources} = this._displayAudio.getAnkiNoteMediaAudioDetails(details.term, details.reading); + const {sources, preferredAudioIndex, enableDefaultAudioSources} = this._displayAudio.getAnkiNoteMediaAudioDetails(details.term, details.reading, isSentence); const languageSummary = this._display.getLanguageSummary(); return { sources, diff --git a/ext/js/display/display-audio.js b/ext/js/display/display-audio.js index e2e048ba2d..ab8e4c8f30 100644 --- a/ext/js/display/display-audio.js +++ b/ext/js/display/display-audio.js @@ -144,13 +144,14 @@ export class DisplayAudio { /** * @param {string} term * @param {string} reading + * @param {boolean} isSentence * @returns {import('display-audio').AudioMediaOptions} */ - getAnkiNoteMediaAudioDetails(term, reading) { + getAnkiNoteMediaAudioDetails(term, reading, isSentence = false) { /** @type {import('display-audio').AudioSourceShort[]} */ const sources = []; let preferredAudioIndex = null; - const primaryCardAudio = this._getPrimaryCardAudio(term, reading); + const primaryCardAudio = this._getPrimaryCardAudio(term, reading, isSentence); if (primaryCardAudio !== null) { const {index, subIndex} = primaryCardAudio; const source = this._audioSources[index]; @@ -351,7 +352,7 @@ export class DisplayAudio { const headwordIndex = this._getAudioPlayButtonHeadwordIndex(button); const dictionaryEntryIndex = this._display.getElementDictionaryEntryIndex(button); - const {detail: {action, item, menu, shiftKey}} = e; + const {detail: {action, item, menu, shiftKey, ctrlKey}} = e; switch (action) { case 'playAudioFromSource': if (shiftKey) { @@ -361,7 +362,11 @@ export class DisplayAudio { break; case 'setPrimaryAudio': e.preventDefault(); - this._setPrimaryAudio(dictionaryEntryIndex, headwordIndex, item, menu, true); + if (ctrlKey) { + this._setPrimaryAudio(dictionaryEntryIndex, headwordIndex, item, menu, true, true); + } else { + this._setPrimaryAudio(dictionaryEntryIndex, headwordIndex, item, menu, true); + } break; } } @@ -379,6 +384,7 @@ export class DisplayAudio { cacheEntry = { sourceMap: new Map(), primaryCardAudio: null, + sentenceCardAudio: null, }; this._cache.set(key, cacheEntry); } @@ -419,7 +425,7 @@ export class DisplayAudio { const headword = this._getHeadword(dictionaryEntryIndex, headwordIndex); if (headword === null) { - return {audio: null, source: null, subIndex: 0, valid: false}; + return {audio: null, source: null, subIndex: 0, isSentence: false, valid: false}; } const buttons = this._getAudioPlayButtons(dictionaryEntryIndex, headwordIndex); @@ -434,10 +440,11 @@ export class DisplayAudio { let title; let source = null; let subIndex = 0; + let isSentence = false; const info = await this._createTermAudio(term, reading, sources, audioInfoListIndex); const valid = (info !== null); if (valid) { - ({audio, source, subIndex} = info); + ({audio, source, subIndex, isSentence} = info); const sourceIndex = sources.indexOf(source); title = `From source ${1 + sourceIndex}: ${source.name}`; } else { @@ -471,7 +478,7 @@ export class DisplayAudio { } } - return {audio, source, subIndex, valid}; + return {audio, source, subIndex, isSentence, valid}; } finally { progressIndicatorVisible.clearOverride(overrideToken); } @@ -489,9 +496,9 @@ export class DisplayAudio { try { const token = this._entriesToken; - const {valid} = await this._playAudio(dictionaryEntryIndex, headwordIndex, [source], subIndex); + const {valid, isSentence} = await this._playAudio(dictionaryEntryIndex, headwordIndex, [source], subIndex); if (valid && token === this._entriesToken) { - this._setPrimaryAudio(dictionaryEntryIndex, headwordIndex, item, null, false); + this._setPrimaryAudio(dictionaryEntryIndex, headwordIndex, item, null, false, isSentence); } } catch (e) { // NOP @@ -504,8 +511,9 @@ export class DisplayAudio { * @param {?HTMLElement} item * @param {?PopupMenu} menu * @param {boolean} canToggleOff + * @param {boolean} isSentence */ - _setPrimaryAudio(dictionaryEntryIndex, headwordIndex, item, menu, canToggleOff) { + _setPrimaryAudio(dictionaryEntryIndex, headwordIndex, item, menu, canToggleOff, isSentence = false) { if (item === null) { return; } const {source, subIndex} = this._getMenuItemSourceInfo(item); if (source === null || !source.downloadable) { return; } @@ -518,16 +526,34 @@ export class DisplayAudio { const cacheEntry = this._getCacheItem(term, reading, true); if (typeof cacheEntry === 'undefined') { return; } - let {primaryCardAudio} = cacheEntry; - primaryCardAudio = ( - !canToggleOff || - primaryCardAudio === null || - primaryCardAudio.index !== index || - primaryCardAudio.subIndex !== subIndex ? - {index: index, subIndex} : - null - ); + let {primaryCardAudio, sentenceCardAudio} = cacheEntry; + if (!isSentence) { + primaryCardAudio = ( + !canToggleOff || + primaryCardAudio === null || + primaryCardAudio.index !== index || + primaryCardAudio.subIndex !== subIndex ? + {index: index, subIndex} : + null + ); + if (sentenceCardAudio !== null && sentenceCardAudio.index === index && sentenceCardAudio.subIndex === subIndex) { + sentenceCardAudio = null; + } + } else { + sentenceCardAudio = ( + !canToggleOff || + sentenceCardAudio === null || + sentenceCardAudio.index !== index || + sentenceCardAudio.subIndex !== subIndex ? + {index: index, subIndex} : + null + ); + if (primaryCardAudio !== null && primaryCardAudio.index === index && primaryCardAudio.subIndex === subIndex) { + primaryCardAudio = null; + } + } cacheEntry.primaryCardAudio = primaryCardAudio; + cacheEntry.sentenceCardAudio = sentenceCardAudio; if (menu !== null) { this._updateMenuPrimaryCardAudio(menu.bodyNode, term, reading); @@ -598,10 +624,10 @@ export class DisplayAudio { sourceInfo.infoList = infoList; } - const {audio, index: subIndex, cacheUpdated: cacheUpdated2} = await this._createAudioFromInfoList(source, infoList, audioInfoListIndex); + const {audio, index: subIndex, cacheUpdated: cacheUpdated2, isSentence} = await this._createAudioFromInfoList(source, infoList, audioInfoListIndex); if (cacheUpdated || cacheUpdated2) { this._updateOpenMenu(); } if (audio !== null) { - return {audio, source, subIndex}; + return {audio, source, subIndex, isSentence}; } } @@ -627,6 +653,7 @@ export class DisplayAudio { audio: null, index: -1, cacheUpdated: false, + isSentence: false, }; for (let i = start; i < end; ++i) { const item = infoList[i]; @@ -653,6 +680,9 @@ export class DisplayAudio { item.audio = audio; } + const {sentence} = item.info; + result.isSentence = typeof sentence === 'string' && sentence.length > 0; + if (audio !== null) { result.audio = audio; result.index = i; @@ -953,11 +983,12 @@ export class DisplayAudio { /** * @param {string} term * @param {string} reading + * @param {boolean} isSentence * @returns {?import('display-audio').PrimaryCardAudio} */ - _getPrimaryCardAudio(term, reading) { + _getPrimaryCardAudio(term, reading, isSentence = false) { const cacheEntry = this._getCacheItem(term, reading, false); - return typeof cacheEntry !== 'undefined' ? cacheEntry.primaryCardAudio : null; + return typeof cacheEntry !== 'undefined' ? (isSentence ? cacheEntry.sentenceCardAudio : cacheEntry.primaryCardAudio) : null; } /** @@ -969,6 +1000,9 @@ export class DisplayAudio { const primaryCardAudio = this._getPrimaryCardAudio(term, reading); const primaryCardAudioIndex = (primaryCardAudio !== null ? primaryCardAudio.index : null); const primaryCardAudioSubIndex = (primaryCardAudio !== null ? primaryCardAudio.subIndex : null); + const sentenceCardAudio = this._getPrimaryCardAudio(term, reading, true); + const sentenceCardAudioIndex = (sentenceCardAudio !== null ? sentenceCardAudio.index : null); + const sentenceCardAudioSubIndex = (sentenceCardAudio !== null ? sentenceCardAudio.subIndex : null); const itemGroups = /** @type {NodeListOf} */ (menuBodyNode.querySelectorAll('.popup-menu-item-group')); for (const node of itemGroups) { const {index, subIndex} = node.dataset; @@ -976,7 +1010,9 @@ export class DisplayAudio { const indexNumber = Number.parseInt(index, 10); const subIndexNumber = typeof subIndex === 'string' ? Number.parseInt(subIndex, 10) : null; const isPrimaryCardAudio = (indexNumber === primaryCardAudioIndex && subIndexNumber === primaryCardAudioSubIndex); + const isSentenceCardAudio = (indexNumber === sentenceCardAudioIndex && subIndexNumber === sentenceCardAudioSubIndex); node.dataset.isPrimaryCardAudio = `${isPrimaryCardAudio}`; + node.dataset.isSentenceCardAudio = `${isSentenceCardAudio}`; } } diff --git a/ext/js/media/audio-downloader.js b/ext/js/media/audio-downloader.js index c061314694..788626d7ab 100644 --- a/ext/js/media/audio-downloader.js +++ b/ext/js/media/audio-downloader.js @@ -115,6 +115,44 @@ export class AudioDownloader { throw error; } + /** + * @param {import('audio').AudioSourceInfo[]} sources + * @param {?number} preferredAudioIndex + * @param {string} term + * @param {string} reading + * @param {?number} idleTimeout + * @param {import('language').LanguageSummary} languageSummary + * @param {boolean} downloadAudio + * @returns {Promise} + */ + async downloadSampleSentenceAudio(sources, preferredAudioIndex, term, reading, idleTimeout, languageSummary, downloadAudio) { + const errors = []; + for (const source of sources) { + let infoList = await this.getTermAudioInfoList(source, term, reading, languageSummary); + if (typeof preferredAudioIndex === 'number') { + infoList = (preferredAudioIndex >= 0 && preferredAudioIndex < infoList.length ? [infoList[preferredAudioIndex]] : []); + } + for (const info of infoList) { + switch (info.type) { + case 'url': + try { + const {sentence} = info; + if (typeof sentence !== 'string' || sentence.length === 0) { continue; } + const audio = downloadAudio ? await this._downloadAudioFromUrl(info.url, source.type, idleTimeout) : null; + return {audio: audio, text: sentence}; + } catch (e) { + errors.push(e); + } + break; + } + } + } + + const error = new ExtensionError('Could not download sample sentence audio'); + error.data = {errors}; + throw error; + } + // Private /** @@ -476,10 +514,11 @@ export class AudioDownloader { /** @type {import('audio-downloader').Info[]} */ const results = []; - for (const {url: url2, name} of responseJson.audioSources) { + for (const {url: url2, name, sentence} of responseJson.audioSources) { /** @type {import('audio-downloader').Info1} */ const info = {type: 'url', url: url2}; if (typeof name === 'string') { info.name = name; } + if (typeof sentence === 'string') { info.sentence = sentence; } results.push(info); } return results; diff --git a/ext/js/templates/template-renderer-media-provider.js b/ext/js/templates/template-renderer-media-provider.js index 216fd371fc..b1cfb623c3 100644 --- a/ext/js/templates/template-renderer-media-provider.js +++ b/ext/js/templates/template-renderer-media-provider.js @@ -98,6 +98,8 @@ export class TemplateRendererMediaProvider { const type = args[0]; switch (type) { case 'audio': return this._getSimpleMediaData(media, 'audio'); + case 'sampleSentenceAudio': return this._getSimpleMediaData(media, 'sampleSentenceAudio'); + case 'sampleSentenceText': return this._getSimpleMediaData(media, 'sampleSentenceText'); case 'screenshot': return this._getSimpleMediaData(media, 'screenshot'); case 'clipboardImage': return this._getSimpleMediaData(media, 'clipboardImage'); case 'clipboardText': return this._getSimpleMediaData(media, 'clipboardText'); diff --git a/ext/templates-modals.html b/ext/templates-modals.html index d6b6b95dfc..b997bc33df 100644 --- a/ext/templates-modals.html +++ b/ext/templates-modals.html @@ -1053,6 +1053,14 @@

Pronunciation Dictionaries

{reading} Kana reading for the term, or empty for terms where the expression is the reading. + + {sample-sentence-audio} + Audio of a sample sentence containing the term from one of the audio sources (if available). + + + {sample-sentence-text} + Sample sentence containing the term from one of the audio sources (if available). + {single-glossary-DICT-NAME} diff --git a/test/data/anki-note-builder-test-results.json b/test/data/anki-note-builder-test-results.json index 6505c3613e..68678b1049 100644 --- a/test/data/anki-note-builder-test-results.json +++ b/test/data/anki-note-builder-test-results.json @@ -116,6 +116,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打cloze-suffix", "sentence-furigana-plain": "cloze-prefix打cloze-suffix" }, @@ -161,6 +163,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打cloze-suffix", "sentence-furigana-plain": "cloze-prefix打cloze-suffix" } @@ -211,6 +215,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打つcloze-suffix", "sentence-furigana-plain": "cloze-prefix打つcloze-suffix" }, @@ -256,6 +262,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打つcloze-suffix", "sentence-furigana-plain": "cloze-prefix打つcloze-suffix" }, @@ -301,6 +309,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打つcloze-suffix", "sentence-furigana-plain": "cloze-prefix打つcloze-suffix" }, @@ -346,6 +356,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打つcloze-suffix", "sentence-furigana-plain": "cloze-prefix打つcloze-suffix" }, @@ -391,6 +403,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打cloze-suffix", "sentence-furigana-plain": "cloze-prefix打cloze-suffix" }, @@ -436,6 +450,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打cloze-suffix", "sentence-furigana-plain": "cloze-prefix打cloze-suffix" } @@ -486,6 +502,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
  3. [-1]
  4. [3]
  5. [2]
  6. [2]
  7. [1]
  8. [1,3]
  9. [1]
  10. [1]
  11. [0]
  12. [3]
  13. [2]
  14. [2]
  15. [0]
  16. [3]
  17. [0]
  18. [0]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ち込むcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ち込むcloze-suffix" }, @@ -531,6 +549,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ち込むcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ち込むcloze-suffix" }, @@ -576,6 +596,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
  3. [-1]
  4. [3]
  5. [2]
  6. [2]
  7. [1]
  8. [1,3]
  9. [1]
  10. [1]
  11. [0]
  12. [3]
  13. [2]
  14. [2]
  15. [0]
  16. [3]
  17. [0]
  18. [0]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ち込むcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ち込むcloze-suffix" }, @@ -621,6 +643,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ち込むcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ち込むcloze-suffix" }, @@ -666,6 +690,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ちcloze-suffix" }, @@ -711,6 +737,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ちcloze-suffix" }, @@ -756,6 +784,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ちcloze-suffix" }, @@ -801,6 +831,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ちcloze-suffix" }, @@ -846,6 +878,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打cloze-suffix", "sentence-furigana-plain": "cloze-prefix打cloze-suffix" }, @@ -891,6 +925,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打cloze-suffix", "sentence-furigana-plain": "cloze-prefix打cloze-suffix" } @@ -941,6 +977,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix画像cloze-suffix", "sentence-furigana-plain": "cloze-prefix画像cloze-suffix" } @@ -991,6 +1029,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixだcloze-suffix", "sentence-furigana-plain": "cloze-prefixだcloze-suffix" } @@ -1041,6 +1081,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixダースcloze-suffix", "sentence-furigana-plain": "cloze-prefixダースcloze-suffix" }, @@ -1086,6 +1128,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixダcloze-suffix", "sentence-furigana-plain": "cloze-prefixダcloze-suffix" } @@ -1136,6 +1180,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixうつcloze-suffix", "sentence-furigana-plain": "cloze-prefixうつcloze-suffix" }, @@ -1181,6 +1227,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixうつcloze-suffix", "sentence-furigana-plain": "cloze-prefixうつcloze-suffix" } @@ -1231,6 +1279,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixぶつcloze-suffix", "sentence-furigana-plain": "cloze-prefixぶつcloze-suffix" }, @@ -1276,6 +1326,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixぶつcloze-suffix", "sentence-furigana-plain": "cloze-prefixぶつcloze-suffix" } @@ -1326,6 +1378,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
  3. [-1]
  4. [3]
  5. [2]
  6. [2]
  7. [1]
  8. [1,3]
  9. [1]
  10. [1]
  11. [0]
  12. [3]
  13. [2]
  14. [2]
  15. [0]
  16. [3]
  17. [0]
  18. [0]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixうちこむcloze-suffix", "sentence-furigana-plain": "cloze-prefixうちこむcloze-suffix" }, @@ -1371,6 +1425,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
  3. [-1]
  4. [3]
  5. [2]
  6. [2]
  7. [1]
  8. [1,3]
  9. [1]
  10. [1]
  11. [0]
  12. [3]
  13. [2]
  14. [2]
  15. [0]
  16. [3]
  17. [0]
  18. [0]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixうちこむcloze-suffix", "sentence-furigana-plain": "cloze-prefixうちこむcloze-suffix" }, @@ -1416,6 +1472,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixうちcloze-suffix", "sentence-furigana-plain": "cloze-prefixうちcloze-suffix" }, @@ -1461,6 +1519,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixうちcloze-suffix", "sentence-furigana-plain": "cloze-prefixうちcloze-suffix" } @@ -1511,6 +1571,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixぶちこむcloze-suffix", "sentence-furigana-plain": "cloze-prefixぶちこむcloze-suffix" }, @@ -1556,6 +1618,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixぶちこむcloze-suffix", "sentence-furigana-plain": "cloze-prefixぶちこむcloze-suffix" }, @@ -1601,6 +1665,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixぶちcloze-suffix", "sentence-furigana-plain": "cloze-prefixぶちcloze-suffix" }, @@ -1646,6 +1712,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixぶちcloze-suffix", "sentence-furigana-plain": "cloze-prefixぶちcloze-suffix" } @@ -1696,6 +1764,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixがぞうcloze-suffix", "sentence-furigana-plain": "cloze-prefixがぞうcloze-suffix" } @@ -1758,6 +1828,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
  3. [-1]
  4. [3]
  5. [2]
  6. [2]
  7. [1]
  8. [1,3]
  9. [1]
  10. [1]
  11. [0]
  12. [3]
  13. [2]
  14. [2]
  15. [0]
  16. [3]
  17. [0]
  18. [0]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ち込むcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ち込むcloze-suffix" }, @@ -1803,6 +1875,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ち込むcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ち込むcloze-suffix" }, @@ -1848,6 +1922,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ちcloze-suffix" }, @@ -1893,6 +1969,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ちcloze-suffix" }, @@ -1938,6 +2016,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打cloze-suffix", "sentence-furigana-plain": "cloze-prefix打cloze-suffix" }, @@ -1983,6 +2063,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打cloze-suffix", "sentence-furigana-plain": "cloze-prefix打cloze-suffix" } @@ -2033,6 +2115,8 @@ "pitch-accent-graphs-jj": "
  1. (うちこむ only)
  2. (うちこむ only)
  3. (うちこむ only)
  4. (うちこむ only)
  5. (うちこむ only)
  6. (うちこむ only)
  7. (うちこむ only)
  8. (うちこむ only)
  9. (うちこむ only)
  10. (うちこむ only)
  11. (うちこむ only)
  12. (うちこむ only)
  13. (うちこむ only)
  14. (うちこむ only)
  15. (うちこむ only)
  16. (うちこむ only)
  17. (うちこむ only)
  18. (うちこむ only)
  19. (ぶちこむ only)
  20. (ぶちこむ only)
", "pitch-accent-positions": "
  1. (うちこむ only) [0]
  2. (うちこむ only) [3]
  3. (うちこむ only) [-1]
  4. (うちこむ only) [3]
  5. (うちこむ only) [2]
  6. (うちこむ only) [2]
  7. (うちこむ only) [1]
  8. (うちこむ only) [1,3]
  9. (うちこむ only) [1]
  10. (うちこむ only) [1]
  11. (うちこむ only) [0]
  12. (うちこむ only) [3]
  13. (うちこむ only) [2]
  14. (うちこむ only) [2]
  15. (うちこむ only) [0]
  16. (うちこむ only) [3]
  17. (うちこむ only) [0]
  18. (うちこむ only) [0]
  19. (ぶちこむ only) [0]
  20. (ぶちこむ only) [3]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ち込むcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ち込むcloze-suffix" }, @@ -2078,6 +2162,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ちcloze-suffix" }, @@ -2123,6 +2209,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打cloze-suffix", "sentence-furigana-plain": "cloze-prefix打cloze-suffix" }, @@ -2168,6 +2256,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打cloze-suffix", "sentence-furigana-plain": "cloze-prefix打cloze-suffix" } @@ -2218,6 +2308,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
  3. [-1]
  4. [3]
  5. [2]
  6. [2]
  7. [1]
  8. [1,3]
  9. [1]
  10. [1]
  11. [0]
  12. [3]
  13. [2]
  14. [2]
  15. [0]
  16. [3]
  17. [0]
  18. [0]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ち込んでいませんでしたcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ち込んでいませんでしたcloze-suffix" }, @@ -2263,6 +2355,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ち込んでいませんでしたcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ち込んでいませんでしたcloze-suffix" }, @@ -2308,6 +2402,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
  3. [-1]
  4. [3]
  5. [2]
  6. [2]
  7. [1]
  8. [1,3]
  9. [1]
  10. [1]
  11. [0]
  12. [3]
  13. [2]
  14. [2]
  15. [0]
  16. [3]
  17. [0]
  18. [0]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ち込んでいませんでしたcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ち込んでいませんでしたcloze-suffix" }, @@ -2353,6 +2449,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ち込んでいませんでしたcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ち込んでいませんでしたcloze-suffix" }, @@ -2398,6 +2496,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ちcloze-suffix" }, @@ -2443,6 +2543,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ちcloze-suffix" }, @@ -2488,6 +2590,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ちcloze-suffix" }, @@ -2533,6 +2637,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打ちcloze-suffix" }, @@ -2578,6 +2684,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打cloze-suffix", "sentence-furigana-plain": "cloze-prefix打cloze-suffix" }, @@ -2623,6 +2731,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打cloze-suffix", "sentence-furigana-plain": "cloze-prefix打cloze-suffix" } @@ -2673,6 +2783,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
  3. [-1]
  4. [3]
  5. [2]
  6. [2]
  7. [1]
  8. [1,3]
  9. [1]
  10. [1]
  11. [0]
  12. [3]
  13. [2]
  14. [2]
  15. [0]
  16. [3]
  17. [0]
  18. [0]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打(う)ち込(こ)むcloze-suffix", "sentence-furigana-plain": "cloze-prefix打(う)ち込(こ)むcloze-suffix" }, @@ -2718,6 +2830,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打(う)ち込(こ)むcloze-suffix", "sentence-furigana-plain": "cloze-prefix打(う)ち込(こ)むcloze-suffix" }, @@ -2763,6 +2877,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
  3. [-1]
  4. [3]
  5. [2]
  6. [2]
  7. [1]
  8. [1,3]
  9. [1]
  10. [1]
  11. [0]
  12. [3]
  13. [2]
  14. [2]
  15. [0]
  16. [3]
  17. [0]
  18. [0]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打(う)ち込(こ)むcloze-suffix", "sentence-furigana-plain": "cloze-prefix打(う)ち込(こ)むcloze-suffix" }, @@ -2808,6 +2924,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打(う)ち込(こ)むcloze-suffix", "sentence-furigana-plain": "cloze-prefix打(う)ち込(こ)むcloze-suffix" }, @@ -2853,6 +2971,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打(う)ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打(う)ちcloze-suffix" }, @@ -2898,6 +3018,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打(う)ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打(う)ちcloze-suffix" }, @@ -2943,6 +3065,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打(う)ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打(う)ちcloze-suffix" }, @@ -2988,6 +3112,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打(う)ちcloze-suffix", "sentence-furigana-plain": "cloze-prefix打(う)ちcloze-suffix" }, @@ -3033,6 +3159,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打(う)cloze-suffix", "sentence-furigana-plain": "cloze-prefix打(う)cloze-suffix" }, @@ -3078,6 +3206,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix打(う)cloze-suffix", "sentence-furigana-plain": "cloze-prefix打(う)cloze-suffix" } @@ -3128,6 +3258,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
  3. [-1]
  4. [3]
  5. [2]
  6. [2]
  7. [1]
  8. [1,3]
  9. [1]
  10. [1]
  11. [0]
  12. [3]
  13. [2]
  14. [2]
  15. [0]
  16. [3]
  17. [0]
  18. [0]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix(打)(ち)(込)(む)cloze-suffix", "sentence-furigana-plain": "cloze-prefix(打)(ち)(込)(む)cloze-suffix" }, @@ -3173,6 +3305,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix(打)(ち)(込)(む)cloze-suffix", "sentence-furigana-plain": "cloze-prefix(打)(ち)(込)(む)cloze-suffix" }, @@ -3218,6 +3352,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
  3. [-1]
  4. [3]
  5. [2]
  6. [2]
  7. [1]
  8. [1,3]
  9. [1]
  10. [1]
  11. [0]
  12. [3]
  13. [2]
  14. [2]
  15. [0]
  16. [3]
  17. [0]
  18. [0]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix(打)(ち)(込)(む)cloze-suffix", "sentence-furigana-plain": "cloze-prefix(打)(ち)(込)(む)cloze-suffix" }, @@ -3263,6 +3399,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [0]
  2. [3]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix(打)(ち)(込)(む)cloze-suffix", "sentence-furigana-plain": "cloze-prefix(打)(ち)(込)(む)cloze-suffix" }, @@ -3308,6 +3446,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix(打)(ち)cloze-suffix", "sentence-furigana-plain": "cloze-prefix(打)(ち)cloze-suffix" }, @@ -3353,6 +3493,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix(打)(ち)cloze-suffix", "sentence-furigana-plain": "cloze-prefix(打)(ち)cloze-suffix" }, @@ -3398,6 +3540,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix(打)(ち)cloze-suffix", "sentence-furigana-plain": "cloze-prefix(打)(ち)cloze-suffix" }, @@ -3443,6 +3587,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix(打)(ち)cloze-suffix", "sentence-furigana-plain": "cloze-prefix(打)(ち)cloze-suffix" }, @@ -3488,6 +3634,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix(打)cloze-suffix", "sentence-furigana-plain": "cloze-prefix(打)cloze-suffix" }, @@ -3533,6 +3681,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix(打)cloze-suffix", "sentence-furigana-plain": "cloze-prefix(打)cloze-suffix" } @@ -3583,6 +3733,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixtestcloze-suffix", "sentence-furigana-plain": "cloze-prefixtestcloze-suffix" } @@ -3633,6 +3785,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixつtestcloze-suffix", "sentence-furigana-plain": "cloze-prefixつtestcloze-suffix" } @@ -3683,6 +3837,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixtestましたcloze-suffix", "sentence-furigana-plain": "cloze-prefixtestましたcloze-suffix" } @@ -3733,6 +3889,8 @@ "pitch-accent-graphs-jj": "
  1. (うちこむ only)
  2. (うちこむ only)
  3. (うちこむ only)
  4. (うちこむ only)
  5. (うちこむ only)
  6. (うちこむ only)
  7. (うちこむ only)
  8. (うちこむ only)
  9. (うちこむ only)
  10. (うちこむ only)
  11. (うちこむ only)
  12. (うちこむ only)
  13. (うちこむ only)
  14. (うちこむ only)
  15. (うちこむ only)
  16. (うちこむ only)
  17. (うちこむ only)
  18. (うちこむ only)
  19. (ぶちこむ only)
  20. (ぶちこむ only)
", "pitch-accent-positions": "
  1. (うちこむ only) [0]
  2. (うちこむ only) [3]
  3. (うちこむ only) [-1]
  4. (うちこむ only) [3]
  5. (うちこむ only) [2]
  6. (うちこむ only) [2]
  7. (うちこむ only) [1]
  8. (うちこむ only) [1,3]
  9. (うちこむ only) [1]
  10. (うちこむ only) [1]
  11. (うちこむ only) [0]
  12. (うちこむ only) [3]
  13. (うちこむ only) [2]
  14. (うちこむ only) [2]
  15. (うちこむ only) [0]
  16. (うちこむ only) [3]
  17. (うちこむ only) [0]
  18. (うちこむ only) [0]
  19. (ぶちこむ only) [0]
  20. (ぶちこむ only) [3]
", "pitch-accent-categories": "heiban,kifuku", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixうちこむcloze-suffix", "sentence-furigana-plain": "cloze-prefixうちこむcloze-suffix" }, @@ -3778,6 +3936,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixうちcloze-suffix", "sentence-furigana-plain": "cloze-prefixうちcloze-suffix" } @@ -3828,6 +3988,8 @@ "pitch-accent-graphs-jj": "
", "pitch-accent-positions": "
  1. [2]
  2. [2]
  3. [0]
", "pitch-accent-categories": "nakadaka,heiban", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixお手前cloze-suffix", "sentence-furigana-plain": "cloze-prefixお手前cloze-suffix" } @@ -3878,6 +4040,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "[3]", "pitch-accent-categories": "nakadaka", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix番号cloze-suffix", "sentence-furigana-plain": "cloze-prefix番号cloze-suffix" } @@ -3928,6 +4092,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "[0]", "pitch-accent-categories": "heiban", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix中腰cloze-suffix", "sentence-furigana-plain": "cloze-prefix中腰cloze-suffix" } @@ -3978,6 +4144,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "[0]", "pitch-accent-categories": "heiban", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix所業cloze-suffix", "sentence-furigana-plain": "cloze-prefix所業cloze-suffix" } @@ -4028,6 +4196,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "[4]", "pitch-accent-categories": "nakadaka", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix土木工事cloze-suffix", "sentence-furigana-plain": "cloze-prefix土木工事cloze-suffix" } @@ -4078,6 +4248,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix好きcloze-suffix", "sentence-furigana-plain": "cloze-prefix好きcloze-suffix" } @@ -4128,6 +4300,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix構造cloze-suffix", "sentence-furigana-plain": "cloze-prefix構造cloze-suffix" } @@ -4178,6 +4352,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixのたもうたcloze-suffix", "sentence-furigana-plain": "cloze-prefixのたもうたcloze-suffix" } @@ -4228,6 +4404,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix39cloze-suffix", "sentence-furigana-plain": "cloze-prefix39cloze-suffix" } @@ -4278,6 +4456,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixEnglishcloze-suffix", "sentence-furigana-plain": "cloze-prefixEnglishcloze-suffix" } @@ -4328,6 +4508,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixUSBcloze-suffix", "sentence-furigana-plain": "cloze-prefixUSBcloze-suffix" } @@ -4378,6 +4560,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixutsucloze-suffix", "sentence-furigana-plain": "cloze-prefixutsucloze-suffix" }, @@ -4423,6 +4607,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixutsucloze-suffix", "sentence-furigana-plain": "cloze-prefixutsucloze-suffix" } @@ -4473,6 +4659,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixウツcloze-suffix", "sentence-furigana-plain": "cloze-prefixウツcloze-suffix" }, @@ -4518,6 +4706,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixウツcloze-suffix", "sentence-furigana-plain": "cloze-prefixウツcloze-suffix" } @@ -4568,6 +4758,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixてきすとcloze-suffix", "sentence-furigana-plain": "cloze-prefixてきすとcloze-suffix" } @@ -4618,6 +4810,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixウツcloze-suffix", "sentence-furigana-plain": "cloze-prefixウツcloze-suffix" }, @@ -4663,6 +4857,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixウツcloze-suffix", "sentence-furigana-plain": "cloze-prefixウツcloze-suffix" } @@ -4713,6 +4909,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixすっっごーーいcloze-suffix", "sentence-furigana-plain": "cloze-prefixすっっごーーいcloze-suffix" } @@ -4763,6 +4961,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixenglishcloze-suffix", "sentence-furigana-plain": "cloze-prefixenglishcloze-suffix" } @@ -4813,6 +5013,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixLANGUAGEcloze-suffix", "sentence-furigana-plain": "cloze-prefixLANGUAGEcloze-suffix" } @@ -4863,6 +5065,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix마시거나cloze-suffix", "sentence-furigana-plain": "cloze-prefix마시거나cloze-suffix" } @@ -4913,6 +5117,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefixenglishcloze-suffix", "sentence-furigana-plain": "cloze-prefixenglishcloze-suffix" } @@ -4963,6 +5169,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix自重cloze-suffix", "sentence-furigana-plain": "cloze-prefix自重cloze-suffix" }, @@ -5008,6 +5216,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix自重cloze-suffix", "sentence-furigana-plain": "cloze-prefix自重cloze-suffix" } @@ -5058,6 +5268,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix自重cloze-suffix", "sentence-furigana-plain": "cloze-prefix自重cloze-suffix" }, @@ -5103,6 +5315,8 @@ "pitch-accent-graphs-jj": "", "pitch-accent-positions": "", "pitch-accent-categories": "", + "sample-sentence-audio": "", + "sample-sentence-text": "", "sentence-furigana": "cloze-prefix自重cloze-suffix", "sentence-furigana-plain": "cloze-prefix自重cloze-suffix" } diff --git a/test/options-util.test.js b/test/options-util.test.js index f774f08303..cbbf097f16 100644 --- a/test/options-util.test.js +++ b/test/options-util.test.js @@ -691,7 +691,7 @@ function createOptionsUpdatedTestData1() { }, ], profileCurrent: 0, - version: 71, + version: 72, global: { database: { prefixWildcardsSupported: false, diff --git a/types/ext/anki-note-builder.d.ts b/types/ext/anki-note-builder.d.ts index 22a4279eff..f29e87bdbf 100644 --- a/types/ext/anki-note-builder.d.ts +++ b/types/ext/anki-note-builder.d.ts @@ -70,7 +70,7 @@ export type GetRenderingDataDetails = { export type CommonData = AnkiTemplatesInternal.CreateDetails; export type RequirementGeneric = { - type: 'audio' | 'screenshot' | 'clipboardImage' | 'clipboardText' | 'popupSelectionText'; + type: 'audio' | 'sampleSentenceAudio' | 'sampleSentenceText' | 'screenshot' | 'clipboardImage' | 'clipboardText' | 'popupSelectionText'; }; export type RequirementTextFurigana = { @@ -99,6 +99,7 @@ export type AudioMediaOptions = { export type MediaOptions = { audio: AudioMediaOptions | null; + sampleSentenceAudio: AudioMediaOptions | null; screenshot: { format: Settings.AnkiScreenshotFormat; quality: number; @@ -131,6 +132,7 @@ export type MinimalApi = { timestamp: Api.ApiParam<'injectAnkiNoteMedia', 'timestamp'>, definitionDetails: Api.ApiParam<'injectAnkiNoteMedia', 'definitionDetails'>, audioDetails: Api.ApiParam<'injectAnkiNoteMedia', 'audioDetails'>, + sampleSentenceAudioDetails: Api.ApiParam<'injectAnkiNoteMedia', 'sampleSentenceAudioDetails'>, screenshotDetails: Api.ApiParam<'injectAnkiNoteMedia', 'screenshotDetails'>, clipboardDetails: Api.ApiParam<'injectAnkiNoteMedia', 'clipboardDetails'>, dictionaryMediaDetails: Api.ApiParam<'injectAnkiNoteMedia', 'dictionaryMediaDetails'>, diff --git a/types/ext/anki-templates.d.ts b/types/ext/anki-templates.d.ts index 53f5ecabf6..32b1ca9aef 100644 --- a/types/ext/anki-templates.d.ts +++ b/types/ext/anki-templates.d.ts @@ -29,6 +29,8 @@ export type Context = { export type Media = { audio?: MediaObject; + sampleSentenceAudio?: MediaObject; + sampleSentenceText?: MediaObject; screenshot?: MediaObject; clipboardImage?: MediaObject; clipboardText?: MediaObject; @@ -41,6 +43,8 @@ export type MediaObject = {value: string}; export type MediaSimpleType = ( 'audio' | + 'sampleSentenceAudio' | + 'sampleSentenceText' | 'screenshot' | 'clipboardImage' | 'clipboardText' | diff --git a/types/ext/api.d.ts b/types/ext/api.d.ts index 3c99f366a8..94c8fc9021 100644 --- a/types/ext/api.d.ts +++ b/types/ext/api.d.ts @@ -80,6 +80,11 @@ export type InjectAnkiNoteMediaDefinitionDetails = InjectAnkiNoteMediaTermDefini export type InjectAnkiNoteMediaAudioDetails = AnkiNoteBuilder.AudioMediaOptions; +export type InjectAnkiNoteMediaSampleSentenceAudioDetails = { + details: InjectAnkiNoteMediaAudioDetails; + downloadAudio: boolean; +}; + export type InjectAnkiNoteMediaScreenshotDetails = { tabId: number; frameId: number; @@ -188,6 +193,7 @@ type ApiSurface = { timestamp: number; definitionDetails: InjectAnkiNoteMediaDefinitionDetails; audioDetails: InjectAnkiNoteMediaAudioDetails | null; + sampleSentenceAudioDetails: InjectAnkiNoteMediaSampleSentenceAudioDetails | null; screenshotDetails: InjectAnkiNoteMediaScreenshotDetails | null; clipboardDetails: InjectAnkiNoteMediaClipboardDetails | null; dictionaryMediaDetails: InjectAnkiNoteMediaDictionaryMediaDetails[]; @@ -197,6 +203,8 @@ type ApiSurface = { clipboardImageFileName: string | null; clipboardText: string | null; audioFileName: string | null; + sampleSentenceAudioFileName: string | null; + sampleSentenceText: string | null; dictionaryMedia: InjectAnkiNoteDictionaryMediaResult[]; errors: Core.SerializedError[]; }; diff --git a/types/ext/audio-downloader.d.ts b/types/ext/audio-downloader.d.ts index dd9496e6e9..87288993fd 100644 --- a/types/ext/audio-downloader.d.ts +++ b/types/ext/audio-downloader.d.ts @@ -31,6 +31,7 @@ export type Info1 = { type: 'url'; url: string; name?: string; + sentence?: string; }; export type Info2 = { @@ -38,6 +39,7 @@ export type Info2 = { text: string; voice: string; name?: undefined; + sentence?: undefined; }; export type AudioBinaryBase64 = { @@ -45,6 +47,11 @@ export type AudioBinaryBase64 = { contentType: string | null; }; +export type SampleSentence = { + audio: AudioBinaryBase64 | null; + text: string | null; +}; + export type CustomAudioList = { type: 'audioSourceList'; audioSources: CustomAudioListSource[]; @@ -53,6 +60,7 @@ export type CustomAudioList = { export type CustomAudioListSource = { url: string; name?: string; + sentence?: string; }; export type WikimediaCommonsLookupResponse = { diff --git a/types/ext/display-audio.d.ts b/types/ext/display-audio.d.ts index 6b7821cc08..2193fe0c01 100644 --- a/types/ext/display-audio.d.ts +++ b/types/ext/display-audio.d.ts @@ -23,6 +23,7 @@ import type * as Settings from './settings'; export type CacheItem = { sourceMap: Map; primaryCardAudio: PrimaryCardAudio | null; + sentenceCardAudio: PrimaryCardAudio | null; }; export type CachedInfoList = { @@ -77,6 +78,7 @@ export type PlayAudioResult = { audio: GenericAudio | null; source: AudioSource | null; subIndex: number; + isSentence: boolean; valid: boolean; }; @@ -84,12 +86,14 @@ export type TermAudio = { audio: GenericAudio; source: AudioSource; subIndex: number; + isSentence: boolean; }; export type CreateAudioResult = { audio: GenericAudio | null; index: number; cacheUpdated: boolean; + isSentence: boolean; }; export type GenericAudio = HTMLAudioElement | TextToSpeechAudio;