forked from akai/readest
fix(tts): avoid race condition in preloadNextSSML causing wrong highlights (#3748)
preloadNextSSML() called tts.next() interleaved with await, creating async gaps where the concurrent #speak() could dispatch marks against corrupted #ranges state (replaced by next() for a different block). Since mark names restart at "0" per block, marks resolved to the wrong text position, causing accidental page turns and highlights far from the current sentence. Fix: gather all tts.next() and tts.prev() calls synchronously (no await between them) so no async code can see the intermediate #ranges state. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -261,15 +261,27 @@ export class TTSController extends EventTarget {
|
||||
const tts = this.view.tts;
|
||||
if (!tts) return;
|
||||
|
||||
const ssmls: string[] = [];
|
||||
// Gather all next SSMLs and rewind synchronously to avoid a race condition:
|
||||
// tts.next() replaces TTS.#ranges (used by setMark() during playback).
|
||||
// If async gaps exist between next()/prev() calls, a concurrent #speak()
|
||||
// can dispatch marks against the wrong #ranges, causing incorrect highlights
|
||||
// and accidental page turns.
|
||||
const rawSsmls: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const ssml = await this.#preprocessSSML(tts.next());
|
||||
const ssml = tts.next();
|
||||
if (!ssml) break;
|
||||
rawSsmls.push(ssml);
|
||||
}
|
||||
for (let i = 0; i < rawSsmls.length; i++) {
|
||||
tts.prev();
|
||||
}
|
||||
|
||||
const ssmls: string[] = [];
|
||||
for (const raw of rawSsmls) {
|
||||
const ssml = await this.#preprocessSSML(raw);
|
||||
if (!ssml) break;
|
||||
ssmls.push(ssml);
|
||||
}
|
||||
for (let i = 0; i < ssmls.length; i++) {
|
||||
tts.prev();
|
||||
}
|
||||
await Promise.all(ssmls.map((ssml) => this.preloadSSML(ssml, new AbortController().signal)));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user