mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
TTS: add seeking to prev/next paragraph
This commit is contained in:
@@ -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')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user