TTS: add seeking to prev/next paragraph

This commit is contained in:
John Factotum
2023-09-28 17:22:09 +08:00
parent fdec0b2037
commit 7ebf99b2cd
+28 -6
View File
@@ -94,7 +94,7 @@ export class View extends HTMLElement {
#speechDoc #speechDoc
#speechRanges #speechRanges
#speechGranularity #speechGranularity
#lastSpeechRange #lastSpeechMark
isFixedLayout = false isFixedLayout = false
lastLocation lastLocation
history = new History() history = new History()
@@ -422,15 +422,21 @@ export class View extends HTMLElement {
this.#speechRanges = new Map(ranges) this.#speechRanges = new Map(ranges)
this.#speechDoc = doc this.#speechDoc = doc
this.#speechGranularity = granularity this.#speechGranularity = granularity
this.#lastSpeechMark = null
this.#ssml = toSSML(markedDoc) this.#ssml = toSSML(markedDoc)
return this.#ssml return this.#ssml
} }
speak(mark) { #getSpeechMarkElement(ssml, mark) {
if (!mark) return new XMLSerializer().serializeToString(this.#ssml) return ssml.querySelector(`mark[name="${CSS.escape(mark)}"`)
}
#speakFromNode(getNode) {
if (!getNode) return new XMLSerializer().serializeToString(this.#ssml)
// clone document
const ssml = document.implementation.createDocument( const ssml = document.implementation.createDocument(
'http://www.w3.org/2001/10/synthesis', 'speak') 'http://www.w3.org/2001/10/synthesis', 'speak')
ssml.documentElement.replaceWith(ssml.importNode(this.#ssml.documentElement, true)) ssml.documentElement.replaceWith(ssml.importNode(this.#ssml.documentElement, true))
let node = ssml.querySelector(`mark[name="${CSS.escape(mark)}"`)?.previousSibling // remove everything before the node
let node = getNode(ssml)?.previousSibling
while (node) { while (node) {
const next = node.previousSibling ?? node.parentNode?.previousSibling const next = node.previousSibling ?? node.parentNode?.previousSibling
node.parentNode.removeChild(node) node.parentNode.removeChild(node)
@@ -438,17 +444,33 @@ export class View extends HTMLElement {
} }
return new XMLSerializer().serializeToString(ssml) return new XMLSerializer().serializeToString(ssml)
} }
startSpeech(mark) {
return this.#speakFromNode(mark ? ssml =>
this.#getSpeechMarkElement(ssml, mark) : null)
}
seekSpeech(dir) {
return this.#speakFromNode(ssml => {
const walker = ssml.createTreeWalker(ssml.documentElement,
NodeFilter.SHOW_ELEMENT, { acceptNode: node => node.localName === 'p'
? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT })
walker.currentNode = this.#getSpeechMarkElement(ssml, this.#lastSpeechMark)
return dir < 1
? (walker.previousNode(), walker.previousNode())
: walker.nextNode()
})
}
getSpeechMarkBefore(range) { getSpeechMarkBefore(range) {
if (range) for (const [name, range_] of this.#speechRanges.entries()) if (range) for (const [name, range_] of this.#speechRanges.entries())
if (range.compareBoundaryPoints(Range.START_TO_START, range_) <= 0) if (range.compareBoundaryPoints(Range.START_TO_START, range_) <= 0)
return name return name
} }
resumeSpeech() { resumeSpeech() {
return this.speak(this.getSpeechMarkBefore(this.#lastSpeechRange)) return this.startSpeech(this.getSpeechMarkBefore(
this.#speechRanges.get(this.#lastSpeechMark)))
} }
hightlightSpeechMark(name) { hightlightSpeechMark(name) {
const range = this.#speechRanges.get(name) const range = this.#speechRanges.get(name)
this.#lastSpeechRange = range this.#lastSpeechMark = name
if (range) this.renderer.scrollToAnchor(range.cloneRange(), true) if (range) this.renderer.scrollToAnchor(range.cloneRange(), true)
else console.warn('Mark not found') else console.warn('Mark not found')
} }