From 18261dc5dadb3b3a64d9d3d60ca149f368ecd63e Mon Sep 17 00:00:00 2001 From: ztimson Date: Mon, 19 Jan 2026 15:29:01 -0500 Subject: [PATCH] Global TTS interrupt suppression (its being a bitch) --- package.json | 2 +- src/tts.ts | 33 ++++++++++----------------------- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index f0f85d8..55566a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ztimson/utils", - "version": "0.28.11", + "version": "0.28.12", "description": "Utility library", "author": "Zak Timson", "license": "MIT", diff --git a/src/tts.ts b/src/tts.ts index 5b600b0..ee7fe2e 100644 --- a/src/tts.ts +++ b/src/tts.ts @@ -2,6 +2,7 @@ import {removeEmojis} from './string.ts'; export class TTS { private static readonly QUALITY_PATTERNS = ['Google', 'Microsoft', 'Samantha', 'Premium', 'Natural', 'Neural']; + private static _errorHandlerInstalled = false; private _currentUtterance: SpeechSynthesisUtterance | null = null; private _voicesLoaded: Promise; @@ -35,8 +36,8 @@ export class TTS { if(this._currentUtterance && value) this._currentUtterance.voice = value; } - /** Create a TTS instance with optional configuration */ constructor(config?: {rate?: number; pitch?: number; volume?: number; voice?: SpeechSynthesisVoice | null}) { + TTS.installErrorHandler(); this._voicesLoaded = this.initializeVoices(); if(config) { if(config.rate !== undefined) this._rate = config.rate; @@ -46,7 +47,14 @@ export class TTS { } } - /** Initializes voice loading and sets default voice if needed */ + private static installErrorHandler(): void { + if(this._errorHandlerInstalled) return; + window.addEventListener('unhandledrejection', (event) => { + if(event.reason?.error === 'interrupted' && event.reason instanceof SpeechSynthesisErrorEvent) event.preventDefault(); + }); + this._errorHandlerInstalled = true; + } + private initializeVoices(): Promise { return new Promise((resolve) => { const voices = window.speechSynthesis.getVoices(); @@ -64,11 +72,6 @@ export class TTS { }); } - /** - * Selects the best available TTS voice, prioritizing high-quality options - * @param lang Speaking language - * @returns Highest quality voice - */ private static bestVoice(lang = 'en'): SpeechSynthesisVoice | undefined { const voices = window.speechSynthesis.getVoices(); for (const pattern of this.QUALITY_PATTERNS) { @@ -78,14 +81,12 @@ export class TTS { return voices.find(v => v.lang.startsWith(lang)); } - /** Cleans text for TTS by removing emojis, markdown and code block */ private static cleanText(text: string): string { return removeEmojis(text) .replace(/```[\s\S]*?```/g, ' code block ') .replace(/[#*_~`]/g, ''); } - /** Creates a speech utterance with current options */ private createUtterance(text: string): SpeechSynthesisUtterance { const cleanedText = TTS.cleanText(text); const utterance = new SpeechSynthesisUtterance(cleanedText); @@ -97,7 +98,6 @@ export class TTS { return utterance; } - /** Speaks text and returns a Promise which resolves once complete */ async speak(text: string): Promise { if(!text.trim()) return Promise.resolve(); await this._voicesLoaded; @@ -117,25 +117,12 @@ export class TTS { }); } - /** Stops all TTS */ stop(): void { if(this._currentUtterance) this._stoppedUtterances.add(this._currentUtterance); window.speechSynthesis.cancel(); this._currentUtterance = null; } - /** - * Initialize a stream that chunks text into sentences and speak them. - * - * @example - * const stream = tts.speakStream(); - * stream.next("Hello "); - * stream.next("World. How"); - * stream.next(" are you?"); - * await stream.done(); - * - * @returns Object with next function for passing chunk of streamed text and done for completing the stream - */ speakStream(): {next: (text: string) => void, done: () => Promise} { let buffer = ''; let streamPromise: Promise = Promise.resolve();