export class StorySpeechNarrator { private synth: SpeechSynthesis | null = null; private currentUtterance: SpeechSynthesisUtterance | null = null; private onEndCallback: (() => void) | null = null; private onWordCallback: ((wordIndex: number) => void) | null = null; constructor() { if (typeof window !== 'undefined' && 'speechSynthesis' in window) { this.synth = window.speechSynthesis; } } public isAvailable(): boolean { return this.synth !== null; } public speak( text: string, options?: { rate?: number; pitch?: number; onEnd?: () => void; onWord?: (index: number) => void; } ) { if (!this.synth) return; this.stop(); const utterance = new SpeechSynthesisUtterance(text); utterance.rate = options?.rate ?? 0.9; // Slightly slower for children's story cadence utterance.pitch = options?.pitch ?? 1.05; // Slightly warmer/friendly pitch // Find warm friendly voice const voices = this.synth.getVoices(); const naturalVoice = voices.find(v => v.lang.startsWith('en') && (v.name.includes('Natural') || v.name.includes('Samantha') || v.name.includes('Karen') || v.name.includes('Daniel') || v.name.includes('Google')) ) || voices.find(v => v.lang.startsWith('en')); if (naturalVoice) { utterance.voice = naturalVoice; } this.onEndCallback = options?.onEnd || null; this.onWordCallback = options?.onWord || null; utterance.onend = () => { this.currentUtterance = null; if (this.onEndCallback) this.onEndCallback(); }; utterance.onerror = () => { this.currentUtterance = null; if (this.onEndCallback) this.onEndCallback(); }; let wordCount = 0; utterance.onboundary = (e) => { if (e.name === 'word') { if (this.onWordCallback) { this.onWordCallback(wordCount); } wordCount++; } }; this.currentUtterance = utterance; this.synth.speak(utterance); } public stop() { if (!this.synth) return; this.synth.cancel(); this.currentUtterance = null; } public pause() { if (this.synth) { this.synth.pause(); } } public resume() { if (this.synth) { this.synth.resume(); } } public isSpeaking(): boolean { return this.synth ? this.synth.speaking : false; } public getCurrentUtterance(): SpeechSynthesisUtterance | null { return this.currentUtterance; } } export const speechNarrator = new StorySpeechNarrator();