From fdec0b2037286abee38144b3fcf3aff38719ee4b Mon Sep 17 00:00:00 2001 From: John Factotum <50942278+johnfactotum@users.noreply.github.com> Date: Thu, 28 Sep 2023 02:02:56 +0800 Subject: [PATCH] TTS: add starting/resuming speaking from mark/range Also: - Fix `ssml:alphabet` attribute being ignored when set on root - Convert `
` to `` --- tts.js | 6 +++++- view.js | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/tts.js b/tts.js index 5d56518..72ddb21 100644 --- a/tts.js +++ b/tts.js @@ -64,6 +64,8 @@ export const toSSML = doc => { el.setAttribute('name', node.dataset.name) } else if (ps.includes(nodeName)) el = ssml.createElementNS(NS.SSML, 'p') + else if (nodeName === 'br') + el = ssml.createElementNS(NS.SSML, 'break') else if (nodeName === 'em' || nodeName === 'strong') el = ssml.createElementNS(NS.SSML, 'emphasis') @@ -93,5 +95,7 @@ export const toSSML = doc => { } return el } - return convert(doc.body, ssml.documentElement) + convert(doc.body, ssml.documentElement, + doc.documentElement.getAttributeNS(NS.SSML, 'alphabet')) + return ssml } diff --git a/view.js b/view.js index 2de3f04..367a04a 100644 --- a/view.js +++ b/view.js @@ -90,7 +90,11 @@ export class View extends HTMLElement { #tocProgress #pageProgress #searchResults = new Map() + #ssml + #speechDoc #speechRanges + #speechGranularity + #lastSpeechRange isFixedLayout = false lastLocation history = new History() @@ -409,17 +413,43 @@ export class View extends HTMLElement { for (const item of list) this.deleteAnnotation(item) this.#searchResults.clear() } - async speak(granularity) { - const { insertMarks, toSSML } = await import('./tts.js') + async initSpeech(granularity) { const doc = this.renderer.getContents()[0].doc + if (this.#speechDoc === doc && this.#speechGranularity === granularity) + return this.#ssml + const { insertMarks, toSSML } = await import('./tts.js') const { doc: markedDoc, ranges } = insertMarks(textWalker, doc, granularity) this.#speechRanges = new Map(ranges) - const ssml = toSSML(markedDoc) + this.#speechDoc = doc + this.#speechGranularity = granularity + this.#ssml = toSSML(markedDoc) + return this.#ssml + } + speak(mark) { + if (!mark) return new XMLSerializer().serializeToString(this.#ssml) + const ssml = document.implementation.createDocument( + 'http://www.w3.org/2001/10/synthesis', 'speak') + ssml.documentElement.replaceWith(ssml.importNode(this.#ssml.documentElement, true)) + let node = ssml.querySelector(`mark[name="${CSS.escape(mark)}"`)?.previousSibling + while (node) { + const next = node.previousSibling ?? node.parentNode?.previousSibling + node.parentNode.removeChild(node) + node = next + } return new XMLSerializer().serializeToString(ssml) } + getSpeechMarkBefore(range) { + if (range) for (const [name, range_] of this.#speechRanges.entries()) + if (range.compareBoundaryPoints(Range.START_TO_START, range_) <= 0) + return name + } + resumeSpeech() { + return this.speak(this.getSpeechMarkBefore(this.#lastSpeechRange)) + } hightlightSpeechMark(name) { const range = this.#speechRanges.get(name) - if (range) this.renderer.scrollToAnchor(range, true) + this.#lastSpeechRange = range + if (range) this.renderer.scrollToAnchor(range.cloneRange(), true) else console.warn('Mark not found') } }