Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3170c73
fix: resumeInfinity/onresume interaction
tuncayuk Nov 26, 2025
15614f1
chore: ensure target is a SpeechSynthesisUtterance in resumeInfinity …
tuncayuk Dec 2, 2025
f35ab5e
fix: ensure infinityTimer is checked against null in clear and resume…
tuncayuk Dec 2, 2025
910df0c
fix: ensure resumeFromKeepAlive is strictly checked for true in onres…
tuncayuk Dec 2, 2025
ecb2ddc
fix: improve utterance management and error handling in speech synthesis
tuncayuk Dec 3, 2025
d43ee72
chore: add placeholder for getVoices method in speech synthesis
tuncayuk Dec 3, 2025
3e56c9d
chore: improve error handling in speak function of speech synthesis
tuncayuk Dec 3, 2025
d2721b6
chore: enhance queue management and debounce handling in announcer
tuncayuk Dec 3, 2025
6aa4a19
fix: add optional chaining to safely check timer state in resumeInfin…
tuncayuk Dec 3, 2025
a1ebc94
fix: enhance stop and clear functions to ensure clean state in speech…
tuncayuk Dec 3, 2025
9910a08
fix: add double-check for utterance existence before resuming in resu…
tuncayuk Dec 3, 2025
624604f
fix: add early return in clear function to handle non-existent state
tuncayuk Dec 4, 2025
fa88731
chore: remove unused pause event handling in speak function
tuncayuk Dec 4, 2025
4fb38de
fix: add cancelPrevious option to AnnouncerUtterance for managing spe…
tuncayuk Dec 4, 2025
cc30b5e
chore: simplify stop api
tuncayuk Dec 4, 2025
3a1a66e
fix: increase debounce duration in processQueue to improve performance
tuncayuk Dec 5, 2025
7d842ee
fix: add waitForSynthReady function to ensure speech synthesis engine…
tuncayuk Dec 5, 2025
41aa289
fix: add enableUtteranceKeepAlive option to improve speech synthesis …
tuncayuk Dec 5, 2025
6f3a1ae
fix: remove previous resolve function in clear method
tuncayuk Dec 8, 2025
2c5255f
fix: remove redundant utterances.delete calls in clear and startKeepA…
tuncayuk Dec 8, 2025
8e9d109
fix: log result of speaking in processQueue for better debugging
tuncayuk Dec 8, 2025
4dc14df
fix: include result in onend callback of speak function for better ha…
tuncayuk Dec 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ declare module '@lightningjs/blits' {
* @default 1
*/
volume?: number,
/**
* Whether to cancel previous announcements when adding this one
*
* @default false
*/
cancelPrevious?: boolean,
/**
* Whether to enable utterance keep-alive (prevents pausing on some platforms)
*
* @default undefined
*/
enableUtteranceKeepAlive?: boolean
}

export interface AnnouncerUtterance<T = any> extends Promise<T> {
Expand Down
71 changes: 67 additions & 4 deletions src/announcer/announcer.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@ let isProcessing = false
let currentId = null
let debounce = null

const isAndroid = /android/i.test((window.navigator || {}).userAgent || '')
const defaultUtteranceKeepAlive = !isAndroid

// Global default utterance options
let globalDefaultOptions = {}
let globalDefaultOptions = {
enableUtteranceKeepAlive: defaultUtteranceKeepAlive,
}

const noopAnnouncement = {
then() {},
Expand All @@ -51,6 +56,11 @@ const toggle = (v) => {
const speak = (message, politeness = 'off', options = {}) => {
if (active === false) return noopAnnouncement

// if cancelPrevious option is set, clear the queue and stop current speech
if (options.cancelPrevious === true) {
clear()
}

return addToQueue(message, politeness, false, options)
}

Expand Down Expand Up @@ -106,18 +116,22 @@ const addToQueue = (message, politeness, delay = false, options = {}) => {
return done
}

let currentResolveFn = null

const processQueue = async () => {
if (isProcessing === true || queue.length === 0) return
isProcessing = true

const { message, resolveFn, delay, id, options = {} } = queue.shift()

currentId = id
currentResolveFn = resolveFn

if (delay) {
setTimeout(() => {
isProcessing = false
currentId = null
currentResolveFn = null
resolveFn('finished')
processQueue()
}, delay)
Expand All @@ -134,37 +148,86 @@ const processQueue = async () => {
...globalDefaultOptions,
...options,
})
.then(() => {
.then((result) => {
Log.debug(`Announcer - finished speaking: "${message}" (id: ${id})`)
Log.debug('Announcer - finished result: ', result)

currentId = null
currentResolveFn = null
isProcessing = false
resolveFn('finished')
processQueue()
})
.catch((e) => {
currentId = null
currentResolveFn = null
isProcessing = false
Log.debug(`Announcer - error ("${e.error}") while speaking: "${message}" (id: ${id})`)
resolveFn(e.error)
processQueue()
})
debounce = null
}, 200)
}, 300)
}
}

const polite = (message, options = {}) => speak(message, 'polite', options)

const assertive = (message, options = {}) => speak(message, 'assertive', options)

// Clear debounce timer
const clearDebounceTimer = () => {
if (debounce !== null) {
clearTimeout(debounce)
debounce = null
}
}

const stop = () => {
Log.debug('Announcer - stop() called')

// Clear debounce timer if speech hasn't started yet
clearDebounceTimer()

// Always cancel speech synthesis to ensure clean state
speechSynthesis.cancel()

// Store resolve function before resetting state
const prevResolveFn = currentResolveFn

// Reset state
currentId = null
currentResolveFn = null
isProcessing = false

// Resolve promise if there was an active utterance
if (prevResolveFn) {
prevResolveFn('interrupted')
}
}

const clear = () => {
Log.debug('Announcer - clear() called')

// Clear debounce timer
clearDebounceTimer()

// Cancel any active speech synthesis
speechSynthesis.cancel()

// Resolve all pending items in queue
while (queue.length > 0) {
const item = queue.shift()
if (item.resolveFn) {
Log.debug(`Announcer - clearing queued item: "${item.message}" (id: ${item.id})`)
item.resolveFn('cleared')
}
}

// Reset state
currentId = null
currentResolveFn = null
isProcessing = false
queue.length = 0
}

const configure = (options = {}) => {
Expand Down
Loading