174 lines
4.4 KiB
TypeScript
174 lines
4.4 KiB
TypeScript
"use client";
|
|
import { createClient } from "@/utils/supabase/client";
|
|
import React, {
|
|
createContext,
|
|
useContext,
|
|
useState,
|
|
useEffect,
|
|
useRef,
|
|
ReactNode,
|
|
} from "react";
|
|
import removeMarkdown from "remove-markdown";
|
|
import { toast } from "sonner";
|
|
|
|
// More robust sentence splitter using Intl.Segmenter for better accuracy.
|
|
function splitIntoSentences(text: string): string[] {
|
|
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
|
|
return Array.from(segmenter.segment(text)).map(
|
|
(segment) => segment.segment
|
|
);
|
|
}
|
|
// Fallback to regex-based splitting if Intl.Segmenter is unavailable.
|
|
return text.match(/[^\.!\?]+[\.!\?]+/g) || [text];
|
|
}
|
|
|
|
interface TTSContextType {
|
|
sentences: string[];
|
|
currentSentence: number;
|
|
voices: any[];
|
|
selectedSpeaker: string;
|
|
status: "ready" | "running" | null;
|
|
setSelectedSpeaker: (speaker: string) => void;
|
|
setCurrentSentence: (index: number) => void;
|
|
playSentence: (index: number) => void;
|
|
skipToSentence: (index: number) => void;
|
|
playInOrder: (index: number) => void;
|
|
pause: () => void;
|
|
resume: () => void;
|
|
stop: () => void;
|
|
}
|
|
|
|
const TTSContext = createContext<TTSContextType | undefined>(undefined);
|
|
|
|
export const TTSProvider = ({
|
|
pages,
|
|
children,
|
|
}: {
|
|
pages: string[];
|
|
children: ReactNode;
|
|
}) => {
|
|
const supabase = createClient();
|
|
// Combine pages and split into sentences.
|
|
const fullText = pages.join("\n");
|
|
const sentences = splitIntoSentences(fullText).filter(
|
|
(sentence) => sentence.trim() !== "\\n" && sentence.trim() !== ""
|
|
);
|
|
|
|
const [currentSentence, setCurrentSentence] = useState(0);
|
|
const audioRef = useRef<HTMLAudioElement>(null);
|
|
|
|
const [selectedSpeaker, setSelectedSpeaker] = useState("af_heart");
|
|
const [voices, setVoices] = useState<any[]>([]);
|
|
const [status, setStatus] = useState<"ready" | "running" | null>("ready");
|
|
|
|
async function generateTTS(sentence: string, index: number): Promise<string> {
|
|
try {
|
|
const { data, error } = await supabase.functions.invoke("generate-tts", {
|
|
body: {
|
|
text: sentence,
|
|
voice: selectedSpeaker,
|
|
index,
|
|
},
|
|
});
|
|
|
|
setStatus("running");
|
|
|
|
const { audioUrl } = data as { audioUrl: string };
|
|
return audioUrl;
|
|
} catch (error) {
|
|
console.error("Error generating TTS:", error);
|
|
toast.error("Failed to generate TTS. Please try again.");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const playSentence = async (index: number) => {
|
|
setCurrentSentence(index);
|
|
|
|
const sentence = removeMarkdown(sentences[index]);
|
|
try {
|
|
const audioUrl = await generateTTS(sentence, index);
|
|
if (audioRef.current) {
|
|
audioRef.current.src = audioUrl;
|
|
await new Promise((res) => {
|
|
audioRef.current!.play();
|
|
audioRef.current!.onended = () => res(true);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("Error playing sentence:", error);
|
|
}
|
|
};
|
|
|
|
const skipToSentence = (index: number) => {
|
|
if (index < 0 || index >= sentences.length) return;
|
|
playSentence(index);
|
|
};
|
|
|
|
const playInOrder = async (index: number) => {
|
|
if (index < 0 || index >= sentences.length) return;
|
|
setCurrentSentence(index);
|
|
|
|
for (let i = index; i < sentences.length; i++) {
|
|
console.log("Playing sentence:", i, sentences[i]);
|
|
try {
|
|
await playSentence(i);
|
|
} catch (error) {
|
|
console.error("Error playing sentence:", error);
|
|
break; // Stop playback on error
|
|
}
|
|
}
|
|
};
|
|
|
|
const pause = () => {
|
|
if (audioRef.current) {
|
|
audioRef.current.pause();
|
|
}
|
|
};
|
|
|
|
const resume = () => {
|
|
if (audioRef.current) {
|
|
audioRef.current.play();
|
|
}
|
|
};
|
|
|
|
const stop = () => {
|
|
if (audioRef.current) {
|
|
audioRef.current.pause();
|
|
audioRef.current.currentTime = 0;
|
|
}
|
|
};
|
|
|
|
const value: TTSContextType = {
|
|
sentences,
|
|
currentSentence,
|
|
voices,
|
|
playSentence,
|
|
skipToSentence,
|
|
selectedSpeaker,
|
|
setSelectedSpeaker,
|
|
setCurrentSentence,
|
|
playInOrder,
|
|
pause,
|
|
resume,
|
|
stop,
|
|
status,
|
|
};
|
|
|
|
return (
|
|
<TTSContext.Provider value={value}>
|
|
{children}
|
|
<audio ref={audioRef} style={{ display: "none" }} />
|
|
</TTSContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useTTS = (): TTSContextType => {
|
|
const context = useContext(TTSContext);
|
|
if (!context) {
|
|
throw new Error("useTTS must be used within a TTSProvider");
|
|
}
|
|
return context;
|
|
};
|