Files
2026-09-14 23:34:36 +08:00

99 lines
2.5 KiB
TypeScript

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();