Upload files to "/"
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user